Browse Source

support history navigation

Le Tan 7 years ago
parent
commit
f6436bfabf

+ 1 - 1
src/resources/icons/cart.svg

@@ -4,7 +4,7 @@
 <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="#010101" d="M169.6,377.6c-22.882,0-41.6,18.718-41.6,41.601c0,22.882,18.718,41.6,41.6,41.6s41.601-18.718,41.601-41.6
+	<path fill="#000000" d="M169.6,377.6c-22.882,0-41.6,18.718-41.6,41.601c0,22.882,18.718,41.6,41.6,41.6s41.601-18.718,41.601-41.6
 		C211.2,396.317,192.481,377.6,169.6,377.6z M48,51.2v41.6h41.6l74.883,151.682l-31.308,50.954c-3.118,5.2-5.2,12.482-5.2,19.765
 		c0,27.85,19.025,41.6,44.825,41.6H416v-40H177.893c-3.118,0-5.2-2.082-5.2-5.2c0-1.036,2.207-5.2,2.207-5.2l20.782-32.8h154.954
 		c15.601,0,29.128-8.317,36.4-21.836l74.882-128.8c1.237-2.461,2.082-6.246,2.082-10.399c0-11.446-9.364-19.765-20.8-19.765H135.364

+ 10 - 0
src/resources/icons/clear_history.svg

@@ -0,0 +1,10 @@
+<?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" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<path style="fill:#C9302C" d="M443.6,387.1L312.4,255.4l131.5-130c5.4-5.4,5.4-14.2,0-19.6l-37.4-37.6c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4
+	L256,197.8L124.9,68.3c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4L68,105.9c-5.4,5.4-5.4,14.2,0,19.6l131.5,130L68.4,387.1
+	c-2.6,2.6-4.1,6.1-4.1,9.8c0,3.7,1.4,7.2,4.1,9.8l37.4,37.6c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1L256,313.1l130.7,131.1
+	c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1l37.4-37.6c2.6-2.6,4.1-6.1,4.1-9.8C447.7,393.2,446.2,389.7,443.6,387.1z"/>
+</svg>

+ 9 - 0
src/resources/icons/history.svg

@@ -0,0 +1,9 @@
+<?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 fill-opacity=".9">
+    <path style="fill:#000000" d="M255.8 48C141 48 48 141.2 48 256s93 208 207.8 208c115 0 208.2-93.2 208.2-208S370.8 48 255.8 48zm.2 374.4c-91.9 0-166.4-74.5-166.4-166.4S164.1 89.6 256 89.6 422.4 164.1 422.4 256 347.9 422.4 256 422.4z"/>
+    <path style="fill:#000000" d="M266.4 152h-31.2v124.8l109.2 65.5 15.6-25.6-93.6-55.5V152z"/>
+</g>
+</svg>

+ 11 - 0
src/resources/icons/pin.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" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<path fill="#000000" d="M331.8,228C331.8,228,331.8,228,331.8,228c-1.2-0.5-2.4-1-3.5-1.7c-7-4-12.2-10.9-13.9-19.2L295.9,89.4l-0.2-5.8
+	c0-7.1,4.1-10.2,10-13l0,0c0.7-0.3,1.4-0.6,2.1-0.9c7.2-3.4,12.1-7.8,12.1-16.3c0-20.1-6.5-21.4-18.2-21.4h-91.3
+	c-11.7,0-18.2,1.2-18.2,21.4c0,8.5,4.9,12.9,12.1,16.3c0.7,0.3,1.4,0.5,2.1,0.9c0,0,0,0,0,0c5.9,2.9,10,6,10,13l-0.2,5.8
+	l-18.5,117.7c-1.7,8.3-6.9,15.2-13.9,19.2c-1.1,0.7-2.3,1.2-3.5,1.7c0,0,0,0,0,0c-19.7,10.2-36.2,30.8-36.2,54.7
+	c0,15.9,3.5,21.3,15.2,21.3H240l12,176h8l12-176h80.8c11.7,0,15.2-4.7,15.2-21.3C368,258.8,351.5,238.2,331.8,228z"/>
+</svg>

+ 5 - 0
src/resources/themes/v_moonlight/v_moonlight.palette

@@ -119,6 +119,10 @@ search_hit_item_bg=@master_dark_bg
 ue_cmd_busy_border=#3F51B5
 ue_cmd_fail_border=@danger_bg
 
+; VListWidget/VTreeWidget separator.
+item_separator_fg=@master_light_bg
+item_separator_bg=@separator_bg
+
 [widgets]
 ; Widget color attributes.
 
@@ -138,6 +142,7 @@ menubar_item_selected_bg=@selected_bg
 ; Menu.
 menu_bg=@base_bg
 menu_fg=@base_fg
+menu_border_bg=@border_bg
 menu_item_disabled_fg=@disabled_fg
 menu_item_selected_fg=@selected_fg
 menu_item_selected_bg=@selected_bg

+ 17 - 9
src/resources/themes/v_moonlight/v_moonlight.qss

@@ -40,7 +40,7 @@ QMenuBar::item:selected {
 QMenu {
     background: @menu_bg;
     color: @menu_fg;
-    margin: 2px;
+    border: 2px solid @menu_border_bg;
 }
 
 QMenu::icon {
@@ -402,10 +402,10 @@ VButtonMenuItem:disabled {
 /* QComboBox */
 QComboBox#NotebookSelector {
     border: none;
-    font-size: 13pt;
-    padding-top: 3px;
-    padding-bottom: 3px;
-    icon-size: $30px;
+    font-size: 12pt;
+    padding-top: 5px;
+    padding-bottom: 5px;
+    icon-size: $24px;
     font-weight: bold;
     color: @combobox_notebookselector_fg;
     background: @combobox_notebookselector_bg;
@@ -424,14 +424,14 @@ QComboBox#NotebookSelector:hover {
 QComboBox#NotebookSelector QListWidget {
     border: 1px solid @combobox_view_border;
     background-color: @combobox_bg;
-    font-size: 13pt;
+    font-size: 12pt;
     font-weight: normal;
-    icon-size: $30px;
+    icon-size: $24px;
 }
 
 QComboBox#NotebookSelector QListWidget::item {
-    padding-top: $10px;
-    padding-bottom: $10px;
+    padding-top: $5px;
+    padding-bottom: $5px;
 }
 
 QComboBox#NotebookSelector QListWidget::item:hover {
@@ -851,6 +851,10 @@ QListView::item:selected:!active {
     color: @listview_item_selected_inactive_fg;
     background: @listview_item_selected_inactive_bg;
 }
+
+QListView::item:disabled {
+    background: transparent;
+}
 /* End QListView */
 
 /* QSplitter */
@@ -1250,6 +1254,10 @@ QWidget[ToolBoxTitle="true"] {
     border-bottom: 2px solid @toolbox_title_border;
 }
 
+QWidget[ToolBoxTitle="true"] QPushButton[ToolBoxTitleBtn="true"] {
+    height: $20px;
+}
+
 QWidget[MainEditor="true"] {
     border: none;
 }

+ 4 - 0
src/resources/themes/v_native/v_native.palette

@@ -87,6 +87,10 @@ search_hit_item_bg=#80CBC4
 ue_cmd_busy_border=#3F51B5
 ue_cmd_fail_border=@danger_bg
 
+; VListWidget/VTreeWidget separator.
+item_separator_fg=@content_fg
+item_separator_bg=@separator_bg
+
 [widgets]
 ; Widget color attributes.
 

+ 8 - 8
src/resources/themes/v_native/v_native.qss

@@ -294,10 +294,10 @@ VButtonMenuItem:disabled {
 /* QComboBox */
 QComboBox#NotebookSelector {
     border: none;
-    font-size: 13pt;
-    padding-top: 3px;
-    padding-bottom: 3px;
-    icon-size: $30px;
+    font-size: 12pt;
+    padding-top: 5px;
+    padding-bottom: 5px;
+    icon-size: $24px;
     background: @combobox_bg;
 }
 
@@ -328,13 +328,13 @@ QComboBox#NotebookSelector::down-arrow:disabled {
 QComboBox#NotebookSelector QListWidget {
     border: 1px solid @combobox_view_border;
     background-color: @combobox_bg;
-    font-size: 13pt;
-    icon-size: $30px;
+    font-size: 12pt;
+    icon-size: $24px;
 }
 
 QComboBox#NotebookSelector QListWidget::item {
-    padding-top: $10px;
-    padding-bottom: $10px;
+    padding-top: $5px;
+    padding-bottom: $5px;
 }
 
 QComboBox#NotebookSelector QListWidget::item:hover {

+ 6 - 1
src/resources/themes/v_pure/v_pure.palette

@@ -113,6 +113,10 @@ search_hit_item_bg=#CCE7E4
 ue_cmd_busy_border=#3F51B5
 ue_cmd_fail_border=@danger_bg
 
+; VListWidget/VTreeWidget separator.
+item_separator_fg=@master_dark_bg
+item_separator_bg=@separator_bg
+
 [widgets]
 ; Widget color attributes.
 
@@ -120,7 +124,7 @@ ue_cmd_fail_border=@danger_bg
 widget_fg=@base_fg
 
 ; Separator of dock widgets.
-dock_separator_bg=@border_bg
+dock_separator_bg=@separator_bg
 dock_separator_hover_bg=@hover_bg
 dock_separator_pressed_bg=@pressed_bg
 
@@ -132,6 +136,7 @@ menubar_item_selected_bg=@selected_bg
 ; Menu.
 menu_bg=@base_bg
 menu_fg=@base_fg
+menu_border_bg=@border_bg
 menu_item_disabled_fg=@disabled_fg
 menu_item_selected_fg=@selected_fg
 menu_item_selected_bg=@selected_bg

+ 17 - 9
src/resources/themes/v_pure/v_pure.qss

@@ -40,7 +40,7 @@ QMenuBar::item:selected {
 QMenu {
     background: @menu_bg;
     color: @menu_fg;
-    margin: 2px;
+    border: 2px solid @menu_border_bg;
 }
 
 QMenu::icon {
@@ -402,10 +402,10 @@ VButtonMenuItem:disabled {
 /* QComboBox */
 QComboBox#NotebookSelector {
     border: none;
-    font-size: 13pt;
-    padding-top: 3px;
-    padding-bottom: 3px;
-    icon-size: $30px;
+    font-size: 12pt;
+    padding-top: 5px;
+    padding-bottom: 5px;
+    icon-size: $24px;
     font-weight: bold;
     color: @combobox_notebookselector_fg;
     background: @combobox_notebookselector_bg;
@@ -424,14 +424,14 @@ QComboBox#NotebookSelector:hover {
 QComboBox#NotebookSelector QListWidget {
     border: 1px solid @combobox_view_border;
     background-color: @combobox_bg;
-    font-size: 13pt;
+    font-size: 12pt;
     font-weight: normal;
-    icon-size: $30px;
+    icon-size: $24px;
 }
 
 QComboBox#NotebookSelector QListWidget::item {
-    padding-top: $10px;
-    padding-bottom: $10px;
+    padding-top: $5px;
+    padding-bottom: $5px;
 }
 
 QComboBox#NotebookSelector QListWidget::item:hover {
@@ -851,6 +851,10 @@ QListView::item:selected:!active {
     color: @listview_item_selected_inactive_fg;
     background: @listview_item_selected_inactive_bg;
 }
+
+QListView::item:disabled {
+    background: transparent;
+}
 /* End QListView */
 
 /* QSplitter */
@@ -1249,6 +1253,10 @@ QWidget[ToolBoxTitle="true"] {
     border-bottom: $2px solid @toolbox_title_border;
 }
 
+QWidget[ToolBoxTitle="true"] QPushButton[ToolBoxTitleBtn="true"] {
+    height: $20px;
+}
+
 QWidget[MainEditor="true"] {
     border: none;
 }

+ 4 - 0
src/resources/vnote.ini

@@ -209,6 +209,10 @@ enable_wildcard_in_simple_search=true
 ; scope,object,target,engine,option,pattern
 search_options=4,2,7,0,0,""
 
+; Number of items in history
+; 0 to disable history
+history_size=50
+
 [export]
 ; Path of the wkhtmltopdf tool
 wkhtmltopdf=wkhtmltopdf

+ 5 - 2
src/src.pro

@@ -131,7 +131,8 @@ SOURCES += main.cpp\
     vlivepreviewhelper.cpp \
     vmathjaxpreviewhelper.cpp \
     vmathjaxwebdocument.cpp \
-    vmathjaxinplacepreviewhelper.cpp
+    vmathjaxinplacepreviewhelper.cpp \
+    vhistorylist.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -254,7 +255,9 @@ HEADERS  += vmainwindow.h \
     vmathjaxpreviewhelper.h \
     vmathjaxwebdocument.h \
     vmathjaxinplacepreviewhelper.h \
-    markdownitoption.h
+    markdownitoption.h \
+    vhistorylist.h \
+    vhistoryentry.h
 
 RESOURCES += \
     vnote.qrc \

+ 33 - 6
src/vcart.cpp

@@ -41,7 +41,7 @@ void VCart::setupUI()
                                                   g_mainWin,
                                                   MessageBoxType::Danger);
                     if (ret == QMessageBox::Ok) {
-                        m_itemList->clear();
+                        m_itemList->clearAll();
                         updateNumberLabel();
                     }
                 }
@@ -55,7 +55,7 @@ void VCart::setupUI()
     btnLayout->addWidget(m_numLabel);
     btnLayout->setContentsMargins(0, 0, 0, 0);
 
-    m_itemList = new QListWidget();
+    m_itemList = new VListWidget();
     m_itemList->setAttribute(Qt::WA_MacShowFocusRect, false);
     m_itemList->setContextMenuPolicy(Qt::CustomContextMenu);
     m_itemList->setSelectionMode(QAbstractItemView::ExtendedSelection);
@@ -109,9 +109,9 @@ void VCart::handleContextMenuRequested(QPoint p_pos)
     menu.setToolTipsVisible(true);
 
     if (item) {
-        int itemCount = m_itemList->selectedItems().size();
-        if (itemCount == 1) {
-            menu.addAction(m_openAct);
+        menu.addAction(m_openAct);
+
+        if (m_itemList->selectedItems().size() == 1) {
             menu.addAction(m_locateAct);
         }
 
@@ -174,7 +174,15 @@ void VCart::deleteSelectedItems()
 
 void VCart::openSelectedItems() const
 {
-    openItem(m_itemList->currentItem());
+    QStringList files;
+    QList<QListWidgetItem *> selectedItems = m_itemList->selectedItems();
+    for (auto it : selectedItems) {
+        files << getFilePath(it);
+    }
+
+    if (!files.isEmpty()) {
+        g_mainWin->openFiles(files);
+    }
 }
 
 void VCart::locateCurrentItem()
@@ -266,3 +274,22 @@ void VCart::updateNumberLabel() const
     m_numLabel->setText(tr("%1 %2").arg(cnt)
                                    .arg(cnt > 1 ? tr("Items") : tr("Item")));
 }
+
+void VCart::showNavigation()
+{
+    VNavigationMode::showNavigation(m_itemList);
+}
+
+bool VCart::handleKeyNavigation(int p_key, bool &p_succeed)
+{
+    static bool secondKey = false;
+    return VNavigationMode::handleKeyNavigation(m_itemList,
+                                                secondKey,
+                                                p_key,
+                                                p_succeed);
+}
+
+QWidget *VCart::getContentWidget() const
+{
+    return m_itemList;
+}

+ 9 - 5
src/vcart.h

@@ -7,14 +7,12 @@
 #include "vnavigationmode.h"
 
 class QPushButton;
-class QListWidget;
+class VListWidget;
 class QListWidgetItem;
 class QLabel;
 class QAction;
-class QKeyEvent;
-class QFocusEvent;
 
-class VCart : public QWidget
+class VCart : public QWidget, public VNavigationMode
 {
     Q_OBJECT
 public:
@@ -26,6 +24,12 @@ public:
 
     QVector<QString> getFiles() const;
 
+    QWidget *getContentWidget() const;
+
+    // Implementations for VNavigationMode.
+    void showNavigation() Q_DECL_OVERRIDE;
+    bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE;
+
 private slots:
     void handleContextMenuRequested(QPoint p_pos);
 
@@ -55,7 +59,7 @@ private:
 
     QPushButton *m_clearBtn;
     QLabel *m_numLabel;
-    QListWidget *m_itemList;
+    VListWidget *m_itemList;
 
     QAction *m_openAct;
     QAction *m_locateAct;

+ 40 - 2
src/vconfigmanager.cpp

@@ -293,6 +293,11 @@ void VConfigManager::initialize()
 
     m_enableGraphviz = getConfigFromSettings("global", "enable_graphviz").toBool();
     m_graphvizDot = getConfigFromSettings("web", "graphviz_dot").toString();
+
+    m_historySize = getConfigFromSettings("global", "history_size").toInt();
+    if (m_historySize < 0) {
+        m_historySize = 0;
+    }
 }
 
 void VConfigManager::initSettings()
@@ -1210,9 +1215,42 @@ void VConfigManager::setLastOpenedFiles(const QVector<VFileSessionInfo> &p_files
     }
 
     m_sessionSettings->endArray();
-    qDebug() << "write" << p_files.size()
-             << "items in [last_opened_files] section";
+}
+
+void VConfigManager::getHistory(QLinkedList<VHistoryEntry> &p_history)
+{
+    p_history.clear();
+
+    int size = m_sessionSettings->beginReadArray("history");
+    for (int i = 0; i < size; ++i) {
+        m_sessionSettings->setArrayIndex(i);
+        p_history.append(VHistoryEntry::fromSettings(m_sessionSettings));
+    }
+
+    m_sessionSettings->endArray();
+}
+
+void VConfigManager::setHistory(const QLinkedList<VHistoryEntry> &p_history)
+{
+    if (m_hasReset) {
+        return;
+    }
+
+    const QString section("history");
+
+    // Clear it first
+    m_sessionSettings->beginGroup(section);
+    m_sessionSettings->remove("");
+    m_sessionSettings->endGroup();
 
+    m_sessionSettings->beginWriteArray(section);
+    int i = 0;
+    for (auto it = p_history.begin(); it != p_history.end(); ++it, ++i) {
+        m_sessionSettings->setArrayIndex(i);
+        it->toSettings(m_sessionSettings);
+    }
+
+    m_sessionSettings->endArray();
 }
 
 QVector<VMagicWord> VConfigManager::getCustomMagicWords()

+ 17 - 0
src/vconfigmanager.h

@@ -7,6 +7,7 @@
 #include <QVector>
 #include <QSettings>
 #include <QHash>
+#include <QLinkedList>
 
 #include "vnotebook.h"
 #include "hgmarkdownhighlighter.h"
@@ -15,6 +16,7 @@
 #include "vfilesessioninfo.h"
 #include "utils/vmetawordmanager.h"
 #include "markdownitoption.h"
+#include "vhistoryentry.h"
 
 class QJsonObject;
 class QString;
@@ -356,6 +358,13 @@ public:
     // Write last opened files to [last_opened_files] of session.ini.
     void setLastOpenedFiles(const QVector<VFileSessionInfo> &p_files);
 
+    // Read history from [history] of session.ini.
+    void getHistory(QLinkedList<VHistoryEntry> &p_history);
+
+    void setHistory(const QLinkedList<VHistoryEntry> &p_history);
+
+    int getHistorySize() const;
+
     // Read custom magic words from [magic_words] section.
     QVector<VMagicWord> getCustomMagicWords();
 
@@ -884,6 +893,9 @@ private:
 
     QString m_plantUMLCmd;
 
+    // Size of history.
+    int m_historySize;
+
     // The name of the config file in each directory, obsolete.
     // Use c_dirConfigFile instead.
     static const QString c_obsoleteDirConfigFile;
@@ -2284,4 +2296,9 @@ inline void VConfigManager::setGraphvizDot(const QString &p_dotPath)
     m_graphvizDot = p_dotPath;
     setConfigToSettings("web", "graphviz_dot", p_dotPath);
 }
+
+inline int VConfigManager::getHistorySize() const
+{
+    return m_historySize;
+}
 #endif // VCONFIGMANAGER_H

+ 17 - 0
src/vdirectorytree.cpp

@@ -13,6 +13,7 @@
 #include "utils/vimnavigationforwidget.h"
 #include "utils/viconutils.h"
 #include "vfilelist.h"
+#include "vhistorylist.h"
 
 extern VMainWindow *g_mainWin;
 
@@ -146,6 +147,13 @@ void VDirectoryTree::initActions()
     connect(m_openLocationAct, &QAction::triggered,
             this, &VDirectoryTree::openDirectoryLocation);
 
+    m_pinToHistoryAct = new QAction(VIconUtils::menuIcon(":/resources/icons/pin.svg"),
+                                    tr("Pin To History"),
+                                    this);
+    m_pinToHistoryAct->setToolTip(tr("Pin selected folder to History"));
+    connect(m_pinToHistoryAct, &QAction::triggered,
+            this, &VDirectoryTree::pinDirectoryToHistory);
+
     m_reloadAct = new QAction(tr("&Reload From Disk"), this);
     m_reloadAct->setToolTip(tr("Reload the content of this folder (or notebook) from disk"));
     connect(m_reloadAct, &QAction::triggered,
@@ -435,6 +443,7 @@ void VDirectoryTree::contextMenuRequested(QPoint pos)
 
     if (item) {
         menu.addAction(m_openLocationAct);
+        menu.addAction(m_pinToHistoryAct);
         menu.addAction(dirInfoAct);
     }
 
@@ -1211,3 +1220,11 @@ VDirectory *VDirectoryTree::currentDirectory() const
 
     return NULL;
 }
+
+void VDirectoryTree::pinDirectoryToHistory()
+{
+    QTreeWidgetItem *curItem = currentItem();
+    V_ASSERT(curItem);
+    g_mainWin->getHistoryList()->pinFolder(getVDirectory(curItem)->fetchPath());
+    g_mainWin->showStatusMessage(tr("1 folder pinned to History"));
+}

+ 4 - 0
src/vdirectorytree.h

@@ -93,6 +93,9 @@ private slots:
     // Sort sub-folders of current item's folder.
     void sortItems();
 
+    // Pin selected directory to History.
+    void pinDirectoryToHistory();
+
 protected:
     void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
 
@@ -176,6 +179,7 @@ private:
     QAction *cutAct;
     QAction *pasteAct;
     QAction *m_openLocationAct;
+    QAction *m_pinToHistoryAct;
     QAction *m_sortAct;
 
     // Reload content from disk.

+ 2 - 1
src/veditarea.cpp

@@ -1131,7 +1131,8 @@ void VEditArea::recordClosedFile(const VFileSessionInfo &p_file)
     }
 
     m_lastClosedFiles.push(p_file);
-    qDebug() << "pushed closed file" << p_file.m_file;
+
+    emit fileClosed(p_file.m_file);
 }
 
 void VEditArea::handleFileTimerTimeout()

+ 3 - 0
src/veditarea.h

@@ -105,6 +105,9 @@ signals:
     // Emit when Vim status updated.
     void vimStatusUpdated(const VVim *p_vim);
 
+    // Emit when @p_file is closed.
+    void fileClosed(const QString &p_file);
+
 protected:
     void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
 

+ 28 - 1
src/veditwindow.cpp

@@ -14,6 +14,7 @@
 #include "vconfigmanager.h"
 #include "utils/viconutils.h"
 #include "vcart.h"
+#include "vhistorylist.h"
 
 extern VConfigManager *g_config;
 extern VMainWindow *g_mainWin;
@@ -167,7 +168,9 @@ void VEditWindow::initTabActions()
                 }
             });
 
-    m_addToCartAct = new QAction(tr("Add To Cart"), this);
+    m_addToCartAct = new QAction(VIconUtils::menuIcon(":/resources/icons/cart.svg"),
+                                 tr("Add To Cart"),
+                                 this);
     m_addToCartAct->setToolTip(tr("Add this note to Cart for further processing"));
     connect(m_addToCartAct, &QAction::triggered,
             this, [this](){
@@ -181,6 +184,24 @@ void VEditWindow::initTabActions()
                 g_mainWin->showStatusMessage(tr("1 note added to Cart"));
             });
 
+    m_pinToHistoryAct = new QAction(VIconUtils::menuIcon(":/resources/icons/pin.svg"),
+                                    tr("Pin To History"),
+                                    this);
+    m_pinToHistoryAct->setToolTip(tr("Pin this note to History"));
+    connect(m_pinToHistoryAct, &QAction::triggered,
+            this, [this](){
+                int tab = this->m_pinToHistoryAct->data().toInt();
+                Q_ASSERT(tab != -1);
+
+                VEditTab *editor = getTab(tab);
+                QPointer<VFile> file = editor->getFile();
+                Q_ASSERT(file);
+                QStringList files;
+                files << file->fetchPath();
+                g_mainWin->getHistoryList()->pinFiles(files);
+                g_mainWin->showStatusMessage(tr("1 note pinned to History"));
+            });
+
     m_openLocationAct = new QAction(tr("Open Note Location"), this);
     m_openLocationAct->setToolTip(tr("Open the folder containing this note in operating system"));
     connect(m_openLocationAct, &QAction::triggered,
@@ -692,6 +713,9 @@ void VEditWindow::tabbarContextMenuRequested(QPoint p_pos)
         m_addToCartAct->setData(tab);
         menu.addAction(m_addToCartAct);
 
+        m_pinToHistoryAct->setData(tab);
+        menu.addAction(m_pinToHistoryAct);
+
         m_noteInfoAct->setData(tab);
         menu.addAction(m_noteInfoAct);
     } else if (file->getType() == FileType::Orphan
@@ -708,6 +732,9 @@ void VEditWindow::tabbarContextMenuRequested(QPoint p_pos)
         m_addToCartAct->setData(tab);
         menu.addAction(m_addToCartAct);
 
+        m_pinToHistoryAct->setData(tab);
+        menu.addAction(m_pinToHistoryAct);
+
         m_noteInfoAct->setData(tab);
         menu.addAction(m_noteInfoAct);
     }

+ 3 - 0
src/veditwindow.h

@@ -231,6 +231,9 @@ private:
     // Add this note to Cart.
     QAction *m_addToCartAct;
 
+    // Pin this note to History.
+    QAction *m_pinToHistoryAct;
+
     // Open the location (the folder containing this file) of this note.
     QAction *m_openLocationAct;
 

+ 28 - 1
src/vfilelist.cpp

@@ -20,6 +20,7 @@
 #include "utils/viconutils.h"
 #include "dialog/vtipsdialog.h"
 #include "vcart.h"
+#include "vhistorylist.h"
 
 extern VConfigManager *g_config;
 extern VNote *g_vnote;
@@ -190,11 +191,20 @@ void VFileList::initActions()
     connect(m_openLocationAct, &QAction::triggered,
             this, &VFileList::openFileLocation);
 
-    m_addToCartAct = new QAction(tr("Add To Cart"), this);
+    m_addToCartAct = new QAction(VIconUtils::menuIcon(":/resources/icons/cart.svg"),
+                                 tr("Add To Cart"),
+                                 this);
     m_addToCartAct->setToolTip(tr("Add selected notes to Cart for further processing"));
     connect(m_addToCartAct, &QAction::triggered,
             this, &VFileList::addFileToCart);
 
+    m_pinToHistoryAct = new QAction(VIconUtils::menuIcon(":/resources/icons/pin.svg"),
+                                    tr("Pin To History"),
+                                    this);
+    m_pinToHistoryAct->setToolTip(tr("Pin selected notes to History"));
+    connect(m_pinToHistoryAct, &QAction::triggered,
+            this, &VFileList::pinFileToHistory);
+
     m_sortAct = new QAction(VIconUtils::menuIcon(":/resources/icons/sort.svg"),
                             tr("&Sort"),
                             this);
@@ -276,6 +286,22 @@ void VFileList::addFileToCart() const
                                    .arg(items.size() > 1 ? tr("notes") : tr("note")));
 }
 
+void VFileList::pinFileToHistory() const
+{
+    QList<QListWidgetItem *> items = fileList->selectedItems();
+
+    QStringList files;
+    for (int i = 0; i < items.size(); ++i) {
+        files << getVFile(items[i])->fetchPath();
+    }
+
+    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 VFileList::fileInfo(VNoteFile *p_file)
 {
     if (!p_file) {
@@ -621,6 +647,7 @@ void VFileList::contextMenuRequested(QPoint pos)
         }
 
         menu.addAction(m_addToCartAct);
+        menu.addAction(m_pinToHistoryAct);
 
         if (selectedSize == 1) {
             menu.addAction(fileInfoAct);

+ 5 - 0
src/vfilelist.h

@@ -89,6 +89,9 @@ private slots:
     // Add selected files to Cart.
     void addFileToCart() const;
 
+    // Add selected files to History and pin them.
+    void pinFileToHistory() const;
+
     // Copy selected files to clipboard.
     // Will put a Json string into the clipboard which contains the information
     // about copied files.
@@ -195,6 +198,8 @@ private:
 
     QAction *m_addToCartAct;
 
+    QAction *m_pinToHistoryAct;
+
     // Context sub-menu of Open With.
     QMenu *m_openWithMenu;
 

+ 64 - 0
src/vhistoryentry.h

@@ -0,0 +1,64 @@
+#ifndef VHISTORYENTRY_H
+#define VHISTORYENTRY_H
+
+#include <QDate>
+#include <QSettings>
+
+namespace HistoryConfig
+{
+    static const QString c_file = "file";
+    static const QString c_date = "date";
+    static const QString c_pinned = "pinned";
+    static const QString c_isFolder = "is_folder";
+}
+
+class VHistoryEntry
+{
+public:
+    VHistoryEntry()
+        : m_isPinned(false),
+          m_isFolder(false)
+    {
+    }
+
+    VHistoryEntry(const QString &p_file,
+                  const QDate &p_date,
+                  bool p_isPinned = false,
+                  bool p_isFolder = false)
+        : m_file(p_file), m_isPinned(p_isPinned), m_isFolder(p_isFolder)
+    {
+        m_date = p_date.toString(Qt::ISODate);
+    }
+
+    // Fetch VHistoryEntry from @p_settings.
+    static VHistoryEntry fromSettings(const QSettings *p_settings)
+    {
+        VHistoryEntry entry;
+        entry.m_file = p_settings->value(HistoryConfig::c_file).toString();
+        entry.m_date = p_settings->value(HistoryConfig::c_date).toString();
+        entry.m_isPinned = p_settings->value(HistoryConfig::c_pinned).toBool();
+        entry.m_isFolder = p_settings->value(HistoryConfig::c_isFolder).toBool();
+        return entry;
+    }
+
+    void toSettings(QSettings *p_settings) const
+    {
+        p_settings->setValue(HistoryConfig::c_file, m_file);
+        p_settings->setValue(HistoryConfig::c_date, m_date);
+        p_settings->setValue(HistoryConfig::c_pinned, m_isPinned);
+        p_settings->setValue(HistoryConfig::c_isFolder, m_isFolder);
+    }
+
+    // File path.
+    QString m_file;
+
+    // Accessed date.
+    // UTC in Qt::ISODate format.
+    QString m_date;
+
+    bool m_isPinned;
+
+    bool m_isFolder;
+};
+
+#endif // VHISTORYENTRY_H

+ 507 - 0
src/vhistorylist.cpp

@@ -0,0 +1,507 @@
+#include "vhistorylist.h"
+
+#include <QtWidgets>
+#include <QDebug>
+
+#include "utils/viconutils.h"
+#include "utils/vutils.h"
+#include "vlistwidget.h"
+#include "vconfigmanager.h"
+#include "vmainwindow.h"
+#include "vcart.h"
+#include "vnote.h"
+#include "vnotefile.h"
+#include "vdirectory.h"
+
+extern VMainWindow *g_mainWin;
+
+extern VConfigManager *g_config;
+
+extern VNote *g_vnote;
+
+VHistoryList::VHistoryList(QWidget *p_parent)
+    : QWidget(p_parent),
+      m_initialized(false),
+      m_updatePending(true),
+      m_currentDate(QDate::currentDate())
+{
+    setupUI();
+}
+
+void VHistoryList::setupUI()
+{
+    m_clearBtn = new QPushButton(VIconUtils::buttonDangerIcon(":/resources/icons/clear_history.svg"), "");
+    m_clearBtn->setToolTip(tr("Clear"));
+    m_clearBtn->setProperty("FlatBtn", true);
+    connect(m_clearBtn, &QPushButton::clicked,
+            this, [this]() {
+                init();
+
+                if (m_histories.size() > 0) {
+                    int ret = VUtils::showMessage(QMessageBox::Warning,
+                                                  tr("Warning"),
+                                                  tr("Are you sure to clear History?"),
+                                                  "",
+                                                  QMessageBox::Ok | QMessageBox::Cancel,
+                                                  QMessageBox::Ok,
+                                                  g_mainWin,
+                                                  MessageBoxType::Danger);
+                    if (ret == QMessageBox::Ok) {
+                        m_histories.clear();
+                        g_config->setHistory(m_histories);
+                        m_updatePending = true;
+                        updateList();
+                    }
+                }
+            });
+
+    QHBoxLayout *btnLayout = new QHBoxLayout;
+    btnLayout->addWidget(m_clearBtn);
+    btnLayout->addStretch();
+    btnLayout->setContentsMargins(0, 0, 0, 0);
+
+    m_itemList = new VListWidget();
+    m_itemList->setAttribute(Qt::WA_MacShowFocusRect, false);
+    m_itemList->setContextMenuPolicy(Qt::CustomContextMenu);
+    m_itemList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+    connect(m_itemList, &QListWidget::customContextMenuRequested,
+            this, &VHistoryList::handleContextMenuRequested);
+    connect(m_itemList, &QListWidget::itemActivated,
+            this, &VHistoryList::openItem);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addLayout(btnLayout);
+    mainLayout->addWidget(m_itemList);
+    mainLayout->setContentsMargins(3, 0, 3, 0);
+
+    setLayout(mainLayout);
+}
+
+void VHistoryList::initActions()
+{
+    m_openAct = new QAction(tr("&Open"), this);
+    m_openAct->setToolTip(tr("Open selected notes"));
+    connect(m_openAct, &QAction::triggered,
+            this, &VHistoryList::openSelectedItems);
+
+    m_locateAct = new QAction(VIconUtils::menuIcon(":/resources/icons/locate_note.svg"),
+                              tr("&Locate To Folder"),
+                              this);
+    m_locateAct->setToolTip(tr("Locate the folder of current note"));
+    connect(m_locateAct, &QAction::triggered,
+            this, &VHistoryList::locateCurrentItem);
+
+    m_pinAct = new QAction(VIconUtils::menuIcon(":/resources/icons/pin.svg"),
+                           tr("Pin"),
+                           this);
+    m_pinAct->setToolTip(tr("Pin selected notes in History"));
+    connect(m_pinAct, &QAction::triggered,
+            this, &VHistoryList::pinSelectedItems);
+
+    m_unpinAct = new QAction(tr("Unpin"), this);
+    m_unpinAct->setToolTip(tr("Unpin selected notes in History"));
+    connect(m_unpinAct, &QAction::triggered,
+            this, &VHistoryList::unpinSelectedItems);
+
+    m_addToCartAct = new QAction(VIconUtils::menuIcon(":/resources/icons/cart.svg"),
+                                 tr("Add To Cart"),
+                                 this);
+    m_addToCartAct->setToolTip(tr("Add selected notes to Cart for further processing"));
+    connect(m_addToCartAct, &QAction::triggered,
+            this, &VHistoryList::addFileToCart);
+}
+
+void VHistoryList::addFile(const QString &p_filePath)
+{
+    init();
+
+    QStringList files;
+    files << p_filePath;
+    addFilesInternal(files, false);
+
+    if (isVisible()) {
+        updateList();
+    }
+}
+
+void VHistoryList::pinFiles(const QStringList &p_files)
+{
+    init();
+
+    addFilesInternal(p_files, true);
+    if (isVisible()) {
+        updateList();
+    }
+}
+
+void VHistoryList::unpinFiles(const QStringList &p_files)
+{
+    init();
+
+    for (auto const & file : p_files) {
+        auto it = findFileInHistory(file);
+        if (it != m_histories.end()) {
+            it->m_isPinned = false;
+        }
+    }
+
+    g_config->setHistory(m_histories);
+    m_updatePending = true;
+}
+
+void VHistoryList::pinFolder(const QString &p_folder)
+{
+    init();
+
+    auto it = findFileInHistory(p_folder);
+    if (it != m_histories.end()) {
+        return;
+    }
+
+    m_histories.append(VHistoryEntry(p_folder, QDate::currentDate(), true, true));
+
+    checkHistorySize();
+
+    g_config->setHistory(m_histories);
+    m_updatePending = true;
+
+    if (isVisible()) {
+        updateList();
+    }
+}
+
+void VHistoryList::addFilesInternal(const QStringList &p_files, bool p_isPinned)
+{
+    for (auto const & file : p_files) {
+        // Find it in existing enries.
+        bool pinnedBefore = false;
+        auto it = findFileInHistory(file);
+        if (it != m_histories.end()) {
+            pinnedBefore = it->m_isPinned;
+            m_histories.erase(it);
+        }
+
+        // Append an entry at the end.
+        bool pin = p_isPinned ? true : (pinnedBefore ? true : false);
+        m_histories.append(VHistoryEntry(file, QDate::currentDate(), pin));
+    }
+
+    checkHistorySize();
+
+    g_config->setHistory(m_histories);
+    m_updatePending = true;
+}
+
+void VHistoryList::checkHistorySize()
+{
+    int numToRemove = m_histories.size() - g_config->getHistorySize();
+    for (auto rit = m_histories.begin(); numToRemove > 0 && rit != m_histories.end();) {
+        if (rit->m_isPinned) {
+            ++rit;
+            continue;
+        }
+
+        rit = m_histories.erase(rit);
+        --numToRemove;
+    }
+}
+
+void VHistoryList::init()
+{
+    if (m_initialized) {
+        return;
+    }
+
+    m_folderIcon = VIconUtils::treeViewIcon(":/resources/icons/dir_item.svg");
+
+    initActions();
+
+    g_config->getHistory(m_histories);
+
+    m_initialized = true;
+    m_updatePending = true;
+}
+
+void VHistoryList::showEvent(QShowEvent *p_event)
+{
+    init();
+
+    if (m_currentDate != QDate::currentDate()) {
+        m_currentDate = QDate::currentDate();
+        m_updatePending = true;
+    }
+
+    updateList();
+
+    QWidget::showEvent(p_event);
+}
+
+QLinkedList<VHistoryEntry>::iterator VHistoryList::findFileInHistory(const QString &p_file)
+{
+    for (QLinkedList<VHistoryEntry>::iterator it = m_histories.begin();
+         it != m_histories.end();
+         ++it) {
+        if (VUtils::equalPath(p_file, it->m_file)) {
+            return it;
+        }
+    }
+
+    return m_histories.end();
+}
+
+struct SeparatorItem
+{
+    SeparatorItem()
+        : m_valid(false)
+    {
+    }
+
+    QString m_date;
+    QListWidgetItem *m_item;
+    bool m_valid;
+};
+
+void VHistoryList::updateList()
+{
+    if (!m_updatePending) {
+        return;
+    }
+
+    m_updatePending = false;
+
+    m_itemList->clearAll();
+
+    if (m_histories.isEmpty()) {
+        return;
+    }
+
+    // Add separators.
+    // Pinned separator.
+    SeparatorItem pinItem;
+    pinItem.m_item = VListWidget::createSeparatorItem(tr("Pinned"));
+    m_itemList->addItem(pinItem.m_item);
+
+    const int sepSize = 4;
+    QString sepText[4] = {tr("Today"), tr("Yesterday"), tr("Last 7 Days"), tr("Older")};
+    QVector<SeparatorItem> seps(sepSize);
+
+    seps[0].m_date = m_currentDate.toString(Qt::ISODate);
+    seps[1].m_date = m_currentDate.addDays(-1).toString(Qt::ISODate);
+    seps[2].m_date = m_currentDate.addDays(-7).toString(Qt::ISODate);
+    // Leave the last string empty.
+
+    for (int i = 0; i < sepSize; ++i) {
+        seps[i].m_item = VListWidget::createSeparatorItem(sepText[i]);
+        m_itemList->addItem(seps[i].m_item);
+    }
+
+    for (auto it = m_histories.cbegin(); it != m_histories.cend(); ++it) {
+        QListWidgetItem *item = new QListWidgetItem(VUtils::fileNameFromPath(it->m_file));
+        item->setToolTip(it->m_file);
+        item->setData(Qt::UserRole, (qulonglong)&(*it));
+
+        if (it->m_isFolder) {
+            item->setIcon(m_folderIcon);
+        }
+
+        if (it->m_isPinned) {
+            m_itemList->insertItem(m_itemList->row(pinItem.m_item) + 1, item);
+            pinItem.m_valid = true;
+            continue;
+        }
+
+        for (int i = 0; i < sepSize; ++i) {
+            if (it->m_date >= seps[i].m_date) {
+                m_itemList->insertItem(m_itemList->row(seps[i].m_item) + 1, item);
+                seps[i].m_valid = true;
+                break;
+            }
+        }
+    }
+
+    // We always display pinned separator.
+
+    for (int i = 0; i < sepSize; ++i) {
+        if (!seps[i].m_valid) {
+            delete seps[i].m_item;
+        }
+    }
+
+    seps.clear();
+}
+
+QWidget *VHistoryList::getContentWidget() const
+{
+    return m_itemList;
+}
+
+void VHistoryList::handleContextMenuRequested(QPoint p_pos)
+{
+    QListWidgetItem *item = m_itemList->itemAt(p_pos);
+    if (!item || VListWidget::isSeparatorItem(item)) {
+        return;
+    }
+
+    QMenu menu(this);
+    menu.setToolTipsVisible(true);
+
+    menu.addAction(m_openAct);
+
+    QList<QListWidgetItem *> selectedItems = m_itemList->selectedItems();
+    if (selectedItems.size() == 1) {
+        menu.addAction(m_locateAct);
+    }
+
+    bool allPinned = true, allUnpinned = true;
+    for (auto const & it : selectedItems) {
+        if (getHistoryEntry(it)->m_isPinned) {
+            allUnpinned = false;
+        } else {
+            allPinned = false;
+        }
+    }
+
+    if (allUnpinned) {
+        menu.addAction(m_pinAct);
+    } else if (allPinned) {
+        menu.addAction(m_unpinAct);
+    }
+
+    menu.addSeparator();
+
+    menu.addAction(m_addToCartAct);
+
+    menu.exec(m_itemList->mapToGlobal(p_pos));
+}
+
+bool VHistoryList::isFolder(const QListWidgetItem *p_item) const
+{
+    return getHistoryEntry(p_item)->m_isFolder;
+}
+
+VHistoryEntry *VHistoryList::getHistoryEntry(const QListWidgetItem *p_item) const
+{
+    return (VHistoryEntry *)p_item->data(Qt::UserRole).toULongLong();
+}
+
+QString VHistoryList::getFilePath(const QListWidgetItem *p_item) const
+{
+    return getHistoryEntry(p_item)->m_file;
+}
+
+void VHistoryList::pinSelectedItems()
+{
+    QStringList files;
+    QList<QListWidgetItem *> selectedItems = m_itemList->selectedItems();
+    for (auto it : selectedItems) {
+        files << getFilePath(it);
+    }
+
+    pinFiles(files);
+}
+
+void VHistoryList::unpinSelectedItems()
+{
+    QStringList files;
+    QList<QListWidgetItem *> selectedItems = m_itemList->selectedItems();
+    for (auto it : selectedItems) {
+        files << getFilePath(it);
+    }
+
+    unpinFiles(files);
+
+    if (isVisible()) {
+        updateList();
+    }
+}
+
+void VHistoryList::openSelectedItems() const
+{
+    QStringList files;
+    QList<QListWidgetItem *> selectedItems = m_itemList->selectedItems();
+
+    if (selectedItems.size() == 1 && isFolder(selectedItems.first())) {
+        // Locate to the folder.
+        VDirectory *dir = g_vnote->getInternalDirectory(getFilePath(selectedItems.first()));
+        if (dir) {
+            g_mainWin->locateDirectory(dir);
+        }
+
+        return;
+    }
+
+    for (auto it : selectedItems) {
+        if (isFolder(it)) {
+            // Skip folders.
+            continue;
+        }
+
+        files << getFilePath(it);
+    }
+
+    if (!files.isEmpty()) {
+        g_mainWin->openFiles(files);
+    }
+}
+
+void VHistoryList::openItem(const QListWidgetItem *p_item) const
+{
+    if (!p_item) {
+        return;
+    }
+
+    if (isFolder(p_item)) {
+        // Locate to the folder.
+        VDirectory *dir = g_vnote->getInternalDirectory(getFilePath(p_item));
+        if (dir) {
+            g_mainWin->locateDirectory(dir);
+        }
+
+        return;
+    }
+
+    QStringList files;
+    files << getFilePath(p_item);
+    g_mainWin->openFiles(files);
+}
+
+void VHistoryList::locateCurrentItem()
+{
+    auto item = m_itemList->currentItem();
+    if (!item) {
+        return;
+    }
+
+    VFile *file = g_vnote->getInternalFile(getFilePath(item));
+    if (file) {
+        g_mainWin->locateFile(file);
+    }
+}
+
+void VHistoryList::showNavigation()
+{
+    VNavigationMode::showNavigation(m_itemList);
+}
+
+bool VHistoryList::handleKeyNavigation(int p_key, bool &p_succeed)
+{
+    static bool secondKey = false;
+    return VNavigationMode::handleKeyNavigation(m_itemList,
+                                                secondKey,
+                                                p_key,
+                                                p_succeed);
+}
+
+void VHistoryList::addFileToCart() const
+{
+    QList<QListWidgetItem *> items = m_itemList->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")));
+}

+ 104 - 0
src/vhistorylist.h

@@ -0,0 +1,104 @@
+#ifndef VHISTORYLIST_H
+#define VHISTORYLIST_H
+
+#include <QWidget>
+#include <QLinkedList>
+#include <QIcon>
+
+#include "vhistoryentry.h"
+#include "vnavigationmode.h"
+
+class QPushButton;
+class VListWidget;
+class QListWidgetItem;
+class QLabel;
+class QAction;
+class QShowEvent;
+
+class VHistoryList : public QWidget, public VNavigationMode
+{
+    Q_OBJECT
+public:
+    explicit VHistoryList(QWidget *p_parent = nullptr);
+
+    QWidget *getContentWidget() const;
+
+    void pinFiles(const QStringList &p_files);
+
+    void pinFolder(const QString &p_folder);
+
+    // Implementations for VNavigationMode.
+    void showNavigation() Q_DECL_OVERRIDE;
+    bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE;
+
+public slots:
+    void addFile(const QString &p_filePath);
+
+protected:
+    void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE;
+
+private slots:
+    void handleContextMenuRequested(QPoint p_pos);
+
+    void openSelectedItems() const;
+
+    void openItem(const QListWidgetItem *p_item) const;
+
+    void pinSelectedItems();
+
+    void unpinSelectedItems();
+
+    void locateCurrentItem();
+
+    // Add selected files to Cart.
+    void addFileToCart() const;
+
+private:
+    void setupUI();
+
+    void initActions();
+
+    // Read data from config file.
+    void init();
+
+    void updateList();
+
+    QLinkedList<VHistoryEntry>::iterator findFileInHistory(const QString &p_file);
+
+    QString getFilePath(const QListWidgetItem *p_item) const;
+
+    VHistoryEntry *getHistoryEntry(const QListWidgetItem *p_item) const;
+
+    bool isFolder(const QListWidgetItem *p_item) const;
+
+    // @p_isPinned won't change it if an item is pinned already.
+    void addFilesInternal(const QStringList &p_files, bool p_isPinned);
+
+    void unpinFiles(const QStringList &p_files);
+
+    void checkHistorySize();
+
+    QPushButton *m_clearBtn;
+    VListWidget *m_itemList;
+
+    QAction *m_openAct;
+    QAction *m_locateAct;
+    QAction *m_pinAct;
+    QAction *m_unpinAct;
+    QAction *m_addToCartAct;
+
+    // Whether data is loaded.
+    bool m_initialized;
+
+    // New files are appended to the end.
+    QLinkedList<VHistoryEntry> m_histories;
+
+    // Whether we need to update the list.
+    bool m_updatePending;
+
+    QDate m_currentDate;
+
+    QIcon m_folderIcon;
+};
+
+#endif // VHISTORYLIST_H

+ 19 - 1
src/vlistwidget.cpp

@@ -8,6 +8,9 @@
 #include "utils/vutils.h"
 #include "utils/vimnavigationforwidget.h"
 #include "vstyleditemdelegate.h"
+#include "vpalette.h"
+
+extern VPalette *g_palette;
 
 VListWidget::VListWidget(QWidget *p_parent)
     : QListWidget(p_parent),
@@ -20,7 +23,7 @@ VListWidget::VListWidget(QWidget *p_parent)
 
     m_searchInput->hide();
 
-    m_delegate = new VStyledItemDelegate(this);
+    m_delegate = new VStyledItemDelegate(this, NULL);
     setItemDelegate(m_delegate);
 }
 
@@ -108,6 +111,10 @@ QList<void *> VListWidget::searchItems(const QString &p_text,
     QList<void *> res;
     res.reserve(items.size());
     for (int i = 0; i < items.size(); ++i) {
+        if (items[i]->type() == ItemTypeSeparator) {
+            continue;
+        }
+
         res.append(items[i]);
     }
 
@@ -212,3 +219,14 @@ QSize VListWidget::sizeHint() const
     }
 }
 
+QListWidgetItem *VListWidget::createSeparatorItem(const QString &p_text)
+{
+    QListWidgetItem *item = new QListWidgetItem(p_text, NULL, ItemTypeSeparator);
+    item->setFlags(Qt::NoItemFlags);
+    return item;
+}
+
+bool VListWidget::isSeparatorItem(const QListWidgetItem *p_item)
+{
+    return p_item->type() == ItemTypeSeparator;
+}

+ 5 - 1
src/vlistwidget.h

@@ -34,10 +34,14 @@ public:
 
     virtual QSize sizeHint() const Q_DECL_OVERRIDE;
 
+    void setFitContent(bool p_enabled);
+
     // Sort @p_list according to @p_sortedIdx.
     static void sortListWidget(QListWidget *p_list, const QVector<int> &p_sortedIdx);
 
-    void setFitContent(bool p_enabled);
+    static QListWidgetItem *createSeparatorItem(const QString &p_text);
+
+    static bool isSeparatorItem(const QListWidgetItem *p_item);
 
 private slots:
     void handleSearchModeTriggered(bool p_inSearchMode, bool p_focus);

+ 29 - 10
src/vmainwindow.cpp

@@ -45,6 +45,7 @@
 #include "vhelpue.h"
 #include "vlistfolderue.h"
 #include "dialog/vfixnotebookdialog.h"
+#include "vhistorylist.h"
 
 extern VConfigManager *g_config;
 
@@ -64,6 +65,8 @@ extern QFile g_logFile;
 
 #define COLOR_PIXMAP_ICON_SIZE 64
 
+#define NAVI_BOX_NOTEBOOKS_IDX 0
+
 
 VMainWindow::VMainWindow(VSingleInstanceGuard *p_guard, QWidget *p_parent)
     : QMainWindow(p_parent),
@@ -145,10 +148,12 @@ void VMainWindow::registerCaptainAndNavigationTargets()
     m_captain->registerNavigationTarget(m_notebookSelector);
     m_captain->registerNavigationTarget(m_dirTree);
     m_captain->registerNavigationTarget(m_fileList);
+    m_captain->registerNavigationTarget(m_historyList);
     m_captain->registerNavigationTarget(m_editArea);
     m_captain->registerNavigationTarget(m_toolBox);
     m_captain->registerNavigationTarget(outline);
     m_captain->registerNavigationTarget(m_snippetList);
+    m_captain->registerNavigationTarget(m_cart);
     m_captain->registerNavigationTarget(m_searcher);
 
     // Register Captain mode targets.
@@ -196,14 +201,7 @@ void VMainWindow::registerCaptainAndNavigationTargets()
 
 void VMainWindow::setupUI()
 {
-    m_naviBox = new VToolBox();
-
-    setupNotebookPanel();
-
-    m_naviBox->addItem(m_nbSplitter,
-                       ":/resources/icons/notebook.svg",
-                       tr("Notebooks"),
-                       m_dirTree);
+    setupNaviBox();
 
     m_editArea = new VEditArea();
     m_editArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
@@ -211,6 +209,9 @@ void VMainWindow::setupUI()
     m_fileList->setEditArea(m_editArea);
     m_dirTree->setEditArea(m_editArea);
 
+    connect(m_editArea, &VEditArea::fileClosed,
+            m_historyList, &VHistoryList::addFile);
+
     // Main Splitter
     m_mainSplitter = new QSplitter();
     m_mainSplitter->setObjectName("MainSplitter");
@@ -265,6 +266,23 @@ void VMainWindow::setupUI()
     initTrayIcon();
 }
 
+void VMainWindow::setupNaviBox()
+{
+    m_naviBox = new VToolBox();
+
+    setupNotebookPanel();
+    m_naviBox->addItem(m_nbSplitter,
+                       ":/resources/icons/notebook.svg",
+                       tr("Notebooks"),
+                       m_dirTree);
+
+    m_historyList = new VHistoryList();
+    m_naviBox->addItem(m_historyList,
+                       ":/resources/icons/history.svg",
+                       tr("History"),
+                       m_historyList->getContentWidget());
+}
+
 void VMainWindow::setupNotebookPanel()
 {
     m_notebookSelector = new VNotebookSelector();
@@ -1265,7 +1283,8 @@ void VMainWindow::initToolsDock()
                        tr("Snippets"));
     m_toolBox->addItem(m_cart,
                        ":/resources/icons/cart.svg",
-                       tr("Cart"));
+                       tr("Cart"),
+                       m_cart->getContentWidget());
 
     m_toolDock->setWidget(m_toolBox);
     addDockWidget(Qt::RightDockWidgetArea, m_toolDock);
@@ -3134,5 +3153,5 @@ void VMainWindow::kickOffStartUpTimer(const QStringList &p_files)
 void VMainWindow::showNotebookPanel()
 {
     changePanelView(PanelViewState::VerticalMode);
-    m_naviBox->setCurrentIndex(0, false);
+    m_naviBox->setCurrentIndex(NAVI_BOX_NOTEBOOKS_IDX, false);
 }

+ 12 - 0
src/vmainwindow.h

@@ -42,6 +42,7 @@ class VCart;
 class VSearcher;
 class QPrinter;
 class VUniversalEntry;
+class VHistoryList;
 
 enum class PanelViewState
 {
@@ -79,6 +80,8 @@ public:
 
     VNotebookSelector *getNotebookSelector() const;
 
+    VHistoryList *getHistoryList() const;
+
     // View and edit the information of @p_file, which is an orphan file.
     void editOrphanFileInfo(VFile *p_file);
 
@@ -199,6 +202,8 @@ protected:
 private:
     void setupUI();
 
+    void setupNaviBox();
+
     void setupNotebookPanel();
 
     void initToolBar();
@@ -442,6 +447,8 @@ private:
 
     VUniversalEntry *m_ue;
 
+    VHistoryList *m_historyList;
+
     // Interval of the shared memory timer in ms.
     static const int c_sharedMemTimerInterval;
 };
@@ -481,6 +488,11 @@ inline VCart *VMainWindow::getCart() const
     return m_cart;
 }
 
+inline VHistoryList *VMainWindow::getHistoryList() const
+{
+    return m_historyList;
+}
+
 inline VDirectoryTree *VMainWindow::getDirectoryTree() const
 {
     return m_dirTree;

+ 1 - 1
src/vnavigationmode.cpp

@@ -70,7 +70,7 @@ QList<QListWidgetItem *> VNavigationMode::getVisibleItems(const QListWidget *p_w
     QList<QListWidgetItem *> items;
     for (int i = 0; i < p_widget->count(); ++i) {
         QListWidgetItem *item = p_widget->item(i);
-        if (!item->isHidden()) {
+        if (!item->isHidden() && item->flags() != Qt::NoItemFlags) {
             items.append(item);
         }
     }

+ 3 - 0
src/vnote.qrc

@@ -214,5 +214,8 @@
         <file>utils/markdown-it/markdown-it-imsize.min.js</file>
         <file>utils/markdown-it/markdown-it-emoji.min.js</file>
         <file>resources/icons/notebook.svg</file>
+        <file>resources/icons/history.svg</file>
+        <file>resources/icons/clear_history.svg</file>
+        <file>resources/icons/pin.svg</file>
     </qresource>
 </RCC>

+ 31 - 1
src/vsearchresulttree.cpp

@@ -9,6 +9,7 @@
 #include "vmainwindow.h"
 #include "vnotefile.h"
 #include "vcart.h"
+#include "vhistorylist.h"
 
 extern VNote *g_vnote;
 
@@ -53,10 +54,19 @@ void VSearchResultTree::initActions()
     connect(m_locateAct, &QAction::triggered,
             this, &VSearchResultTree::locateCurrentItem);
 
-    m_addToCartAct = new QAction(tr("Add To Cart"), this);
+    m_addToCartAct = new QAction(VIconUtils::menuIcon(":/resources/icons/cart.svg"),
+                                 tr("Add To Cart"),
+                                 this);
     m_addToCartAct->setToolTip(tr("Add selected notes to Cart for further processing"));
     connect(m_addToCartAct, &QAction::triggered,
             this, &VSearchResultTree::addSelectedItemsToCart);
+
+    m_pinToHistoryAct = new QAction(VIconUtils::menuIcon(":/resources/icons/pin.svg"),
+                                    tr("Pin To History"),
+                                    this);
+    m_pinToHistoryAct->setToolTip(tr("Pin selected notes to History"));
+    connect(m_pinToHistoryAct, &QAction::triggered,
+            this, &VSearchResultTree::pinSelectedItemsToHistory);
 }
 
 void VSearchResultTree::updateResults(const QList<QSharedPointer<VSearchResultItem> > &p_items)
@@ -165,6 +175,7 @@ void VSearchResultTree::handleContextMenuRequested(QPoint p_pos)
 
     if (hasNote) {
         menu.addAction(m_addToCartAct);
+        menu.addAction(m_pinToHistoryAct);
     }
 
     menu.exec(mapToGlobal(p_pos));
@@ -207,6 +218,25 @@ void VSearchResultTree::addSelectedItemsToCart()
     }
 }
 
+void VSearchResultTree::pinSelectedItemsToHistory()
+{
+    QList<QTreeWidgetItem *> items = selectedItems();
+    QStringList files;
+    for (int i = 0; i < items.size(); ++i) {
+        const QSharedPointer<VSearchResultItem> &resItem = itemResultData(items[i]);
+        if (resItem->m_type == VSearchResultItem::Note) {
+            files << resItem->m_path;
+        }
+    }
+
+    if (!files.isEmpty()) {
+        g_mainWin->getHistoryList()->pinFiles(files);
+        g_mainWin->showStatusMessage(tr("%1 %2 pinned to History")
+                                       .arg(files.size())
+                                       .arg(files.size() > 1 ? tr("notes") : tr("note")));
+    }
+}
+
 VSearchResultItem::ItemType VSearchResultTree::itemResultType(const QTreeWidgetItem *p_item) const
 {
     Q_ASSERT(p_item);

+ 4 - 0
src/vsearchresulttree.h

@@ -34,6 +34,8 @@ private slots:
 
     void addSelectedItemsToCart();
 
+    void pinSelectedItemsToHistory();
+
 private:
     void appendItem(const QSharedPointer<VSearchResultItem> &p_item);
 
@@ -56,6 +58,8 @@ private:
     QAction *m_locateAct;
 
     QAction *m_addToCartAct;
+
+    QAction *m_pinToHistoryAct;
 };
 
 #endif // VSEARCHRESULTTREE_H

+ 3 - 2
src/vsnippetlist.cpp

@@ -10,6 +10,7 @@
 #include "dialog/vconfirmdeletiondialog.h"
 #include "vmainwindow.h"
 #include "utils/viconutils.h"
+#include "vlistwidget.h"
 
 extern VConfigManager *g_config;
 
@@ -70,7 +71,7 @@ void VSnippetList::setupUI()
     btnLayout->addWidget(m_numLabel);
     btnLayout->setContentsMargins(0, 0, 0, 0);
 
-    m_snippetList = new QListWidget();
+    m_snippetList = new VListWidget();
     m_snippetList->setAttribute(Qt::WA_MacShowFocusRect, false);
     m_snippetList->setContextMenuPolicy(Qt::CustomContextMenu);
     m_snippetList->setSelectionMode(QAbstractItemView::ExtendedSelection);
@@ -380,7 +381,7 @@ void VSnippetList::makeSureFolderExist() const
 
 void VSnippetList::updateContent()
 {
-    m_snippetList->clear();
+    m_snippetList->clearAll();
 
     for (int i = 0; i < m_snippets.size(); ++i) {
         const VSnippet &snip = m_snippets[i];

+ 2 - 2
src/vsnippetlist.h

@@ -9,7 +9,7 @@
 #include "vnavigationmode.h"
 
 class QPushButton;
-class QListWidget;
+class VListWidget;
 class QListWidgetItem;
 class QLabel;
 class QAction;
@@ -92,7 +92,7 @@ private:
     QPushButton *m_addBtn;
     QPushButton *m_locateBtn;
     QLabel *m_numLabel;
-    QListWidget *m_snippetList;
+    VListWidget *m_snippetList;
 
     QAction *m_applyAct;
     QAction *m_infoAct;

+ 34 - 3
src/vstyleditemdelegate.cpp

@@ -1,17 +1,25 @@
 #include "vstyleditemdelegate.h"
 
 #include <QPainter>
-#include <QDebug>
+#include <QListWidget>
+#include <QListWidgetItem>
+#include <QTreeWidgetItem>
 
 #include "vpalette.h"
+#include "vtreewidget.h"
 
 extern VPalette *g_palette;
 
-VStyledItemDelegate::VStyledItemDelegate(QObject *p_parent)
-    : QStyledItemDelegate(p_parent)
+VStyledItemDelegate::VStyledItemDelegate(QListWidget *p_list, VTreeWidget *p_tree)
+    : QStyledItemDelegate(p_list ? (QObject *)p_list : (QObject *)p_tree),
+      m_list(p_list),
+      m_tree(p_tree)
 {
+    Q_ASSERT(!(m_list && m_tree));
     m_itemHitBg = QBrush(QColor(g_palette->color("search_hit_item_bg")));
     m_itemHitFg = QBrush(QColor(g_palette->color("search_hit_item_fg")));
+    m_itemSeparatorBg = QBrush(QColor(g_palette->color("item_separator_bg")));
+    m_itemSeparatorFg = QBrush(QColor(g_palette->color("item_separator_fg")));
 }
 
 void VStyledItemDelegate::paint(QPainter *p_painter,
@@ -25,7 +33,30 @@ void VStyledItemDelegate::paint(QPainter *p_painter,
         // option.palette.setBrush(QPalette::Base, m_itemHitBg);
         option.palette.setBrush(QPalette::Text, m_itemHitFg);
         QStyledItemDelegate::paint(p_painter, option, p_index);
+    } else if (itemType(p_index) == ItemTypeSeparator) {
+        QStyleOptionViewItem option(p_option);
+        p_painter->fillRect(option.rect, m_itemSeparatorBg);
+        option.palette.setBrush(QPalette::Text, m_itemSeparatorFg);
+        QStyledItemDelegate::paint(p_painter, option, p_index);
     } else {
         QStyledItemDelegate::paint(p_painter, p_option, p_index);
     }
 }
+
+int VStyledItemDelegate::itemType(const QModelIndex &p_index) const
+{
+    int type = 0;
+    if (m_list) {
+        QListWidgetItem *item = m_list->item(p_index.row());
+        if (item) {
+            type = item->type();
+        }
+    } else if (m_tree) {
+        QTreeWidgetItem *item = m_tree->getItemFromIndex(p_index);
+        if (item) {
+            type = item->type();
+        }
+    }
+
+    return type;
+}

+ 15 - 2
src/vstyleditemdelegate.h

@@ -5,11 +5,16 @@
 #include <QBrush>
 #include <QSet>
 
+class QListWidget;
+class VTreeWidget;
+
+// Should be larger then QListWidgetItem::UserType and QTreeWidgetItem::UserType.
+#define ItemTypeSeparator 2000
 
 class VStyledItemDelegate : public QStyledItemDelegate
 {
 public:
-    explicit VStyledItemDelegate(QObject *p_parent = Q_NULLPTR);
+    explicit VStyledItemDelegate(QListWidget *p_list = nullptr, VTreeWidget *p_tree = nullptr);
 
     virtual void paint(QPainter *p_painter,
                        const QStyleOptionViewItem &p_option,
@@ -22,11 +27,19 @@ public:
 private:
     bool isHit(const QModelIndex &p_index) const;
 
-    QBrush m_itemHitBg;
+    int itemType(const QModelIndex &p_index) const;
 
+    QBrush m_itemHitBg;
     QBrush m_itemHitFg;
 
+    QBrush m_itemSeparatorBg;
+    QBrush m_itemSeparatorFg;
+
     QSet<QModelIndex> m_hitItems;
+
+    // m_list OR m_tree (but not both) could be not NULL.
+    const QListWidget *m_list;
+    const VTreeWidget *m_tree;
 };
 
 inline void VStyledItemDelegate::setHitItems(const QSet<QModelIndex> &p_hitItems)

+ 1 - 0
src/vtoolbox.cpp

@@ -52,6 +52,7 @@ int VToolBox::addItem(QWidget *p_widget,
     QPushButton *btn = new QPushButton(icon, "");
     btn->setToolTip(p_text);
     btn->setProperty("FlatBtn", true);
+    btn->setProperty("ToolBoxTitleBtn", true);
     connect(btn, &QPushButton::clicked,
             this, [this]() {
                 QObject *btn = sender();

+ 5 - 1
src/vtreewidget.cpp

@@ -44,7 +44,7 @@ VTreeWidget::VTreeWidget(QWidget *p_parent)
                 effect->setOpacity(SEARCH_INPUT_IDLE_OPACITY);
             });
 
-    m_delegate = new VStyledItemDelegate(this);
+    m_delegate = new VStyledItemDelegate(NULL, this);
     setItemDelegate(m_delegate);
 
     connect(this, &VTreeWidget::itemExpanded,
@@ -155,6 +155,10 @@ QList<void *> VTreeWidget::searchItems(const QString &p_text,
     QList<void *> res;
     res.reserve(items.size());
     for (int i = 0; i < items.size(); ++i) {
+        if (items[i]->type() == ItemTypeSeparator) {
+            continue;
+        }
+
         res.append(items[i]);
     }
 

+ 7 - 0
src/vtreewidget.h

@@ -43,6 +43,8 @@ public:
 
     void setFitContent(bool p_enabled);
 
+    QTreeWidgetItem *getItemFromIndex(const QModelIndex &p_index) const;
+
 protected:
     void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
 
@@ -95,4 +97,9 @@ inline void VTreeWidget::setFitContent(bool p_enabled)
     setSizeAdjustPolicy(m_fitContent ? QAbstractScrollArea::AdjustToContents
                                      : QAbstractScrollArea::AdjustIgnored);
 }
+
+inline QTreeWidgetItem *VTreeWidget::getItemFromIndex(const QModelIndex &p_index) const
+{
+    return itemFromIndex(p_index);
+}
 #endif // VTREEWIDGET_H

+ 0 - 2
src/vvimindicator.cpp

@@ -16,8 +16,6 @@
 
 extern VConfigManager *g_config;
 
-extern VPalette *g_palette;
-
 VVimIndicator::VVimIndicator(QWidget *p_parent)
     : QWidget(p_parent), m_vim(NULL)
 {