Browse Source

support search

- Ctrl+E C to toggle the search dock;
Le Tan 7 years ago
parent
commit
a2ee5413a1

+ 12 - 11
src/dialog/vexportdialog.cpp

@@ -75,7 +75,7 @@ void VExportDialog::setupUI()
     // Notes to export.
     m_srcCB = VUtils::getComboBox();
     m_srcCB->setToolTip(tr("Choose notes to export"));
-    m_srcCB->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
+    m_srcCB->setSizeAdjustPolicy(QComboBox::AdjustToContents);
     connect(m_srcCB, SIGNAL(currentIndexChanged(int)),
             this, SLOT(handleCurrentSrcChanged(int)));
 
@@ -580,7 +580,7 @@ void VExportDialog::startExport()
 
         if (opt.m_outputSuffix.isEmpty()
             || opt.m_cmd.isEmpty()
-            || opt.m_allInOne && opt.m_folderSep.isEmpty()) {
+            || (opt.m_allInOne && opt.m_folderSep.isEmpty())) {
             appendLogLine(tr("Invalid configurations for custom export."));
             m_inExport = false;
             m_exportBtn->setEnabled(true);
@@ -1274,11 +1274,11 @@ int VExportDialog::doExportCustomAllInOne(const QList<QString> &p_files,
 QWidget *VExportDialog::setupCustomAdvancedSettings()
 {
     // Source format.
-    m_customSrcFormatCB = VUtils::getComboBox();
+    m_customSrcFormatCB = VUtils::getComboBox(this);
     m_customSrcFormatCB->setToolTip(tr("Choose format of the input"));
 
     // Output suffix.
-    m_customSuffixEdit = new VLineEdit();
+    m_customSuffixEdit = new VLineEdit(this);
     m_customSuffixEdit->setPlaceholderText(tr("Without the preceding dot"));
     m_customSuffixEdit->setToolTip(tr("Suffix of the output file without the preceding dot"));
     QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
@@ -1288,11 +1288,12 @@ QWidget *VExportDialog::setupCustomAdvancedSettings()
     QLabel *tipsLabel = new QLabel(tr("<span><span style=\"font-weight:bold;\">%0</span> for the input file; "
                                       "<span style=\"font-weight:bold;\">%1</span> for the output file; "
                                       "<span style=\"font-weight:bold;\">%2</span> for the rendering CSS style file; "
-                                      "<span style=\"font-weight:bold;\">%3</span> for the input file directory.</span>"));
+                                      "<span style=\"font-weight:bold;\">%3</span> for the input file directory.</span>"),
+                                   this);
     tipsLabel->setWordWrap(true);
 
     // Enable All In One.
-    m_customAllInOneCB = new QCheckBox(tr("Enable All In One"));
+    m_customAllInOneCB = new QCheckBox(tr("Enable All In One"), this);
     m_customAllInOneCB->setToolTip(tr("Pass a list of input files to the custom command"));
     connect(m_customAllInOneCB, &QCheckBox::stateChanged,
             this, [this](int p_state) {
@@ -1302,13 +1303,13 @@ QWidget *VExportDialog::setupCustomAdvancedSettings()
             });
 
     // Input directory separator.
-    m_customFolderSepEdit = new VLineEdit();
+    m_customFolderSepEdit = new VLineEdit(this);
     m_customFolderSepEdit->setPlaceholderText(tr("Separator to concatenate input files directories"));
     m_customFolderSepEdit->setToolTip(tr("Separator to concatenate input files directories"));
     m_customFolderSepEdit->setEnabled(false);
 
     // Target file name for all in one.
-    m_customTargetFileNameEdit = new VLineEdit();
+    m_customTargetFileNameEdit = new VLineEdit(this);
     m_customTargetFileNameEdit->setPlaceholderText(tr("Empty to use the name of the first source file"));
     m_customTargetFileNameEdit->setToolTip(tr("Name of the generated All-In-One file"));
     validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
@@ -1317,7 +1318,7 @@ QWidget *VExportDialog::setupCustomAdvancedSettings()
     m_customTargetFileNameEdit->setEnabled(false);
 
     // Cmd edit.
-    m_customCmdEdit = new QPlainTextEdit();
+    m_customCmdEdit = new QPlainTextEdit(this);
     m_customCmdEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
     QString cmdExamp("pandoc --resource-path=.:\"%3\" --css=\"%2\" -s -o \"%1\" \"%0\"");
     m_customCmdEdit->setPlaceholderText(cmdExamp);
@@ -1348,12 +1349,12 @@ QWidget *VExportDialog::setupCustomAdvancedSettings()
     QWidget *wid = new QWidget();
     wid->setLayout(advLayout);
 
-    m_customCmdEdit->setMaximumHeight(100);
+    m_customCmdEdit->setMaximumHeight(m_customSrcFormatCB->height() * 3);
 
     return wid;
 }
 
-int VExportDialog::outputAsHTML(QString &p_outputFolder,
+int VExportDialog::outputAsHTML(const QString &p_outputFolder,
                                 QString *p_errMsg,
                                 QList<QString> *p_outputFiles)
 {

+ 1 - 1
src/dialog/vexportdialog.h

@@ -368,7 +368,7 @@ private:
 
     ExportFormat currentFormat() const;
 
-    int outputAsHTML(QString &p_outputFolder,
+    int outputAsHTML(const QString &p_outputFolder,
                      QString *p_errMsg = NULL,
                      QList<QString> *p_outputFiles = NULL);
 

+ 3 - 3
src/dialog/vfindreplacedialog.cpp

@@ -49,7 +49,7 @@ void VFindReplaceDialog::setupUI()
     m_replaceFindBtn->setProperty("FlatBtn", true);
     m_replaceAllBtn = new QPushButton(tr("Replace A&ll"));
     m_replaceAllBtn->setProperty("FlatBtn", true);
-    m_advancedBtn = new QPushButton(tr("&Advanced >>"));
+    m_advancedBtn = new QPushButton(tr("&Advanced >>>"));
     m_advancedBtn->setProperty("FlatBtn", true);
     m_advancedBtn->setCheckable(true);
 
@@ -199,9 +199,9 @@ void VFindReplaceDialog::handleFindTextChanged(const QString &p_text)
 void VFindReplaceDialog::advancedBtnToggled(bool p_checked)
 {
     if (p_checked) {
-        m_advancedBtn->setText("B&asic <<");
+        m_advancedBtn->setText(tr("B&asic <<<"));
     } else {
-        m_advancedBtn->setText("&Advanced <<");
+        m_advancedBtn->setText(tr("&Advanced >>>"));
     }
 
     m_caseSensitiveCheck->setVisible(p_checked);

+ 1 - 0
src/dialog/vfindreplacedialog.h

@@ -48,6 +48,7 @@ private slots:
 
 private:
     void setupUI();
+
     // Bit OR of FindOption
     uint m_options;
     bool m_replaceAvailable;

+ 34 - 0
src/isearchengine.h

@@ -0,0 +1,34 @@
+#ifndef ISEARCHENGINE_H
+#define ISEARCHENGINE_H
+
+#include <QObject>
+#include <QVector>
+
+#include "vsearchconfig.h"
+
+// Abstract class for search engine.
+class ISearchEngine : public QObject
+{
+    Q_OBJECT
+public:
+    explicit ISearchEngine(QObject *p_parent = nullptr)
+        : QObject(p_parent)
+    {
+    }
+
+    virtual void search(const QSharedPointer<VSearchConfig> &p_config,
+                        const QSharedPointer<VSearchResult> &p_result) = 0;
+
+    virtual void stop() = 0;
+
+    virtual void clear() = 0;
+
+signals:
+    void finished(const QSharedPointer<VSearchResult> &p_result);
+
+    void resultItemAdded(const QSharedPointer<VSearchResultItem> &p_item);
+
+protected:
+    QSharedPointer<VSearchResult> m_result;
+};
+#endif // ISEARCHENGINE_H

+ 10 - 0
src/resources/icons/clear_search.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>

+ 15 - 0
src/resources/icons/note_item.svg

@@ -0,0 +1,15 @@
+<?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">
+<g>
+	<path style="fill:#000000" d="M398.6,169.2c-0.9-2.2-2-4.3-3.5-6.1l-83.8-91.7c-1.9-2.1-4.2-3.6-6.7-4.9c-2.9-1.5-6.1-2.1-9.5-2.1H135.2
+		c-12.4,0-22.7,10.6-22.7,23.9v335.2c0,13.4,10.3,24.9,22.7,24.9h243.1c12.4,0,22.2-11.5,22.2-24.9V179.4
+		C400.5,175.8,400,172.3,398.6,169.2z M160.5,178.6c0-1.5,1.8-2.1,3.4-2.1h70.8c1.6,0,2.8,0.6,2.8,2.1v10.8c0,1.4-1.1,3.1-2.8,3.1
+		h-70.8c-1.6,0-3.4-1.7-3.4-3.1V178.6z M160.5,306.6c0-1.5,1.8-2.1,3.4-2.1h122.2c1.6,0,2.4,0.6,2.4,2.1v10.8c0,1.4-0.7,3.1-2.4,3.1
+		H163.9c-1.6,0-3.4-1.7-3.4-3.1V306.6z M320.5,381.4c0,1.4-0.7,3.1-2.4,3.1H163.9c-1.6,0-3.4-1.7-3.4-3.1v-10.8
+		c0-1.5,1.8-2.1,3.4-2.1h154.2c1.6,0,2.4,0.6,2.4,2.1V381.4z M352.5,253.4c0,1.4-0.7,3.1-2.4,3.1H163.9c-1.6,0-3.4-1.7-3.4-3.1
+		v-10.8c0-1.5,1.8-2.1,3.4-2.1h186.2c1.6,0,2.4,0.6,2.4,2.1V253.4z M305.6,177.5c-5.6,0-11.1-5.2-11.1-11.3v-66l71.2,77.3H305.6z"/>
+</g>
+</svg>

+ 10 - 0
src/resources/icons/search.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:#000000" d="M445,386.7l-84.8-85.9c13.8-24.1,21-50.9,21-77.9c0-87.6-71.2-158.9-158.6-158.9C135.2,64,64,135.3,64,222.9
+	c0,87.6,71.2,158.9,158.6,158.9c27.9,0,55.5-7.7,80.1-22.4l84.4,85.6c1.9,1.9,4.6,3.1,7.3,3.1c2.7,0,5.4-1.1,7.3-3.1l43.3-43.8
+	C449,397.1,449,390.7,445,386.7z M222.6,125.9c53.4,0,96.8,43.5,96.8,97c0,53.5-43.4,97-96.8,97c-53.4,0-96.8-43.5-96.8-97
+	C125.8,169.4,169.2,125.9,222.6,125.9z"/>
+</svg>

+ 14 - 0
src/resources/icons/search_advanced.svg

@@ -0,0 +1,14 @@
+<?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 style="fill:#000000" d="M32,376h283.35c6.186-14.112,20.281-24,36.65-24s30.465,9.888,36.65,24H480v32h-91.35c-6.186,14.112-20.281,24-36.65,24
+		s-30.465-9.888-36.65-24H32"/>
+	<path style="fill:#000000" d="M32,240h91.35c6.186-14.112,20.281-24,36.65-24s30.465,9.888,36.65,24H480v32H196.65c-6.186,14.112-20.281,24-36.65,24
+		s-30.465-9.888-36.65-24H32"/>
+	<path style="fill:#000000" d="M32,104h283.35c6.186-14.112,20.281-24,36.65-24s30.465,9.888,36.65,24H480v32h-91.35c-6.186,14.112-20.281,24-36.65,24
+		s-30.465-9.888-36.65-24H32"/>
+</g>
+</svg>

+ 12 - 0
src/resources/icons/search_console.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" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<path style="fill:#000000" d="M468.7,64H43.3c-6,0-11.3,5-11.3,11.1v265.7c0,6.2,5.2,11.1,11.3,11.1h425.4c6,0,11.3-5,11.3-11.1V75.1
+		C480,69,474.8,64,468.7,64z M448,320H64V96h384V320z"/>
+	<path style="fill:#000000" d="M302.5,448c28-0.5,41.5-3.9,29-12.5c-12.5-8.7-28.5-15.3-29-22.5c-0.3-3.7-1.7-45-1.7-45H256h-44.8c0,0-1.5,41.3-1.7,45
+		c-0.5,7.1-16.5,13.8-29,22.5c-12.5,8.7,1,12,29,12.5H302.5z"/>
+</g>
+</svg>

+ 113 - 109
src/resources/themes/v_moonlight/v_moonlight.qss

@@ -183,64 +183,18 @@ QDockWidget::close-button:hover, QDockWidget::float-button:hover {
 /* End DockWidget */
 
 /* QPushButton */
-QPushButton {
-    color: @pushbutton_fg;
-    background: @pushbutton_bg;
-    border: 1px solid @pushbutton_border;
-    padding: 3px;
-    min-width: 80px;
-}
-
-QPushButton:focus {
-    background-color: @pushbutton_focus_bg;
-}
-
-QPushButton:pressed {
-    background-color: @pushbutton_pressed_bg;
-}
-
-QPushButton:checked {
-    background-color: @pushbutton_checked_bg;
-}
-
-QPushButton:checked:hover {
-    background-color: @pushbutton_hover_bg;
-}
-
-QPushButton:hover {
-    background-color: @pushbutton_hover_bg;
-}
-
-QPushButton:flat {
-    border: none;
-}
-
-QPushButton:default {
-    border: 1px solid @pushbutton_default_border;
-}
-
-QPushButton:disabled {
-    color: @pushbutton_disabled_fg;
-    background-color: @pushbutton_disabled_bg;
-}
-
-QPushButton::menu-indicator {
-    image: url(arrow_dropdown.svg);
-    width: 16px;
-    height: 16px;
-}
-
 QPushButton[SpecialBtn="true"] {
     color: @pushbutton_specialbtn_fg;
     background: @pushbutton_specialbtn_bg;
 }
 
-QPushButton[SpecialBtn="true"]:focus {
-    background-color: @pushbutton_specialbtn_focus_bg;
+QPushButton[SpecialBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: @pushbutton_specialbtn_bg;
 }
 
-QPushButton[SpecialBtn="true"]:pressed {
-    background-color: @pushbutton_specialbtn_pressed_bg;
+QPushButton[SpecialBtn="true"]:focus {
+    background-color: @pushbutton_specialbtn_focus_bg;
 }
 
 QPushButton[SpecialBtn="true"]:checked {
@@ -255,16 +209,15 @@ QPushButton[SpecialBtn="true"]:hover {
     background-color: @pushbutton_specialbtn_hover_bg;
 }
 
+QPushButton[SpecialBtn="true"]:pressed {
+    background-color: @pushbutton_specialbtn_pressed_bg;
+}
+
 QPushButton[SpecialBtn="true"]:disabled {
     color: @pushbutton_disabled_fg;
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[SpecialBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: @pushbutton_specialbtn_bg;
-}
-
 QPushButton[CornerBtn="true"] {
     padding: 4px -2px 4px -2px;
     margin: 0px;
@@ -277,14 +230,19 @@ QPushButton[CornerBtn="true"]::menu-indicator {
     image: none;
 }
 
-QPushButton[CornerBtn="true"]:hover {
-    background-color: @pushbutton_cornerbtn_hover_bg;
+QPushButton[CornerBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: transparent;
 }
 
 QPushButton[CornerBtn="true"]:focus {
     background-color: @pushbutton_cornerbtn_focus_bg;
 }
 
+QPushButton[CornerBtn="true"]:hover {
+    background-color: @pushbutton_cornerbtn_hover_bg;
+}
+
 QPushButton[CornerBtn="true"]:pressed {
     background-color: @pushbutton_cornerbtn_pressed_bg;
 }
@@ -294,11 +252,6 @@ QPushButton[CornerBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[CornerBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: transparent;
-}
-
 QPushButton[StatusBtn="true"] {
     font: bold;
     padding: 0px 2px 0px 2px;
@@ -308,14 +261,19 @@ QPushButton[StatusBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[StatusBtn="true"]:hover {
-    background-color: @pushbutton_statusbtn_hover_bg;
+QPushButton[StatusBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: transparent;
 }
 
 QPushButton[StatusBtn="true"]:focus {
     background-color: @pushbutton_statusbtn_focus_bg;;
 }
 
+QPushButton[StatusBtn="true"]:hover {
+    background-color: @pushbutton_statusbtn_hover_bg;
+}
+
 QPushButton[StatusBtn="true"]:pressed {
     background-color: @pushbutton_statusbtn_pressed_bg;
 }
@@ -325,11 +283,6 @@ QPushButton[StatusBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[StatusBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: transparent;
-}
-
 QPushButton[FlatBtn="true"] {
     padding: 4px;
     margin: 0px;
@@ -338,14 +291,19 @@ QPushButton[FlatBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[FlatBtn="true"]:hover {
-    background-color: @pushbutton_flatbtn_hover_bg;
+QPushButton[FlatBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: transparent;
 }
 
 QPushButton[FlatBtn="true"]:focus {
     background-color: @pushbutton_flatbtn_focus_bg;
 }
 
+QPushButton[FlatBtn="true"]:hover {
+    background-color: @pushbutton_flatbtn_hover_bg;
+}
+
 QPushButton[FlatBtn="true"]:pressed {
     background-color: @pushbutton_flatbtn_pressed_bg;
 }
@@ -355,11 +313,6 @@ QPushButton[FlatBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[FlatBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: transparent;
-}
-
 QPushButton[SelectionBtn="true"] {
     padding: 4px 10px 4px 10px;
     border: none;
@@ -369,14 +322,19 @@ QPushButton[SelectionBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[SelectionBtn="true"]:hover {
-    background-color: @pushbutton_selectionbtn_hover_bg;
+QPushButton[SelectionBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: transparent;
 }
 
 QPushButton[SelectionBtn="true"]:focus {
     background-color: @pushbutton_selectionbtn_focus_bg;
 }
 
+QPushButton[SelectionBtn="true"]:hover {
+    background-color: @pushbutton_selectionbtn_hover_bg;
+}
+
 QPushButton[SelectionBtn="true"]:pressed {
     background-color: @pushbutton_selectionbtn_pressed_bg;
 }
@@ -386,11 +344,6 @@ QPushButton[SelectionBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[SelectionBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: transparent;
-}
-
 QPushButton[TitleBtn="true"] {
     padding: 4px;
     margin: 0px;
@@ -399,14 +352,19 @@ QPushButton[TitleBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[TitleBtn="true"]:hover {
-    background-color: @pushbutton_titlebtn_hover_bg;
+QPushButton[TitleBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: @pushbutton_titlebtn_bg;
 }
 
 QPushButton[TitleBtn="true"]:focus {
     background-color: @pushbutton_titlebtn_focus_bg;
 }
 
+QPushButton[TitleBtn="true"]:hover {
+    background-color: @pushbutton_titlebtn_hover_bg;
+}
+
 QPushButton[TitleBtn="true"]:pressed {
     background-color: @pushbutton_titlebtn_pressed_bg;
 }
@@ -416,11 +374,6 @@ QPushButton[TitleBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[TitleBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: @pushbutton_titlebtn_bg;
-}
-
 QPushButton[DangerBtn="true"] {
     color: @pushbutton_dangerbtn_fg;
     border: none;
@@ -428,14 +381,23 @@ QPushButton[DangerBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[DangerBtn="true"]:hover {
-    background-color: @pushbutton_dangerbtn_hover_bg;
+QPushButton[DangerBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: @pushbutton_dangerbtn_bg;
 }
 
 QPushButton[DangerBtn="true"]:focus {
     background-color: @pushbutton_dangerbtn_focus_bg;
 }
 
+QPushButton[DangerBtn="true"]:checked {
+    background-color: @pushbutton_dangerbtn_pressed_bg;
+}
+
+QPushButton[DangerBtn="true"]:hover {
+    background-color: @pushbutton_dangerbtn_hover_bg;
+}
+
 QPushButton[DangerBtn="true"]:pressed {
     background-color: @pushbutton_dangerbtn_pressed_bg;
 }
@@ -445,11 +407,6 @@ QPushButton[DangerBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[DangerBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: @pushbutton_dangerbtn_bg;
-}
-
 QPushButton[ToolBoxActiveBtn="true"] {
     padding: 4px 10px 4px 4px;
     margin: 0px;
@@ -459,6 +416,11 @@ QPushButton[ToolBoxActiveBtn="true"] {
     min-width: -1;
 }
 
+QPushButton[ToolBoxActiveBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: @pushbutton_toolboxbtn_active_bg;
+}
+
 QPushButton[ToolBoxActiveBtn="true"]:focus {
     background-color: @pushbutton_toolboxbtn_active_focus_bg;
 }
@@ -476,11 +438,6 @@ QPushButton[ToolBoxActiveBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[ToolBoxActiveBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: @pushbutton_toolboxbtn_active_bg;
-}
-
 QPushButton[AvatarBtn="true"] {
     padding: 2px 4px 2px 4px;
     margin: 0px;
@@ -489,14 +446,19 @@ QPushButton[AvatarBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[AvatarBtn="true"]:hover {
-    background-color: @pushbutton_avatarbtn_hover_bg;
+QPushButton[AvatarBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: transparent;
 }
 
 QPushButton[AvatarBtn="true"]:focus {
     background-color: @pushbutton_avatarbtn_focus_bg;;
 }
 
+QPushButton[AvatarBtn="true"]:hover {
+    background-color: @pushbutton_avatarbtn_hover_bg;
+}
+
 QPushButton[AvatarBtn="true"]:pressed {
     background-color: @pushbutton_avatarbtn_pressed_bg;
 }
@@ -506,9 +468,51 @@ QPushButton[AvatarBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[AvatarBtn="true"]:default {
+QPushButton {
+    color: @pushbutton_fg;
+    background: @pushbutton_bg;
+    border: 1px solid @pushbutton_border;
+    padding: 3px;
+    min-width: 80px;
+}
+
+QPushButton:default {
     border: 1px solid @pushbutton_default_border;
-    background-color: transparent;
+}
+
+QPushButton:focus {
+    background-color: @pushbutton_focus_bg;
+}
+
+QPushButton:checked {
+    background-color: @pushbutton_checked_bg;
+}
+
+QPushButton:checked:hover {
+    background-color: @pushbutton_hover_bg;
+}
+
+QPushButton:flat {
+    border: none;
+}
+
+QPushButton:hover {
+    background-color: @pushbutton_hover_bg;
+}
+
+QPushButton:pressed {
+    background-color: @pushbutton_pressed_bg;
+}
+
+QPushButton:disabled {
+    color: @pushbutton_disabled_fg;
+    background-color: @pushbutton_disabled_bg;
+}
+
+QPushButton::menu-indicator {
+    image: url(arrow_dropdown.svg);
+    width: 16px;
+    height: 16px;
 }
 
 VButtonMenuItem {
@@ -544,11 +548,11 @@ VButtonMenuItem[Heading6="true"] {
     font-size: 14pt;
 }
 
-VButtonMenuItem:hover {
+VButtonMenuItem:focus {
     background-color: @menubar_item_selected_bg;
 }
 
-VButtonMenuItem:focus {
+VButtonMenuItem:hover {
     background-color: @menubar_item_selected_bg;
 }
 

+ 118 - 114
src/resources/themes/v_pure/v_pure.qss

@@ -183,64 +183,18 @@ QDockWidget::close-button:hover, QDockWidget::float-button:hover {
 /* End DockWidget */
 
 /* QPushButton */
-QPushButton {
-    color: @pushbutton_fg;
-    background: @pushbutton_bg;
-    border: 1px solid @pushbutton_border;
-    padding: 3px;
-    min-width: 80px;
-}
-
-QPushButton:focus {
-    background-color: @pushbutton_focus_bg;
-}
-
-QPushButton:pressed {
-    background-color: @pushbutton_pressed_bg;
-}
-
-QPushButton:checked {
-    background-color: @pushbutton_checked_bg;
-}
-
-QPushButton:checked:hover {
-    background-color: @pushbutton_hover_bg;
-}
-
-QPushButton:hover {
-    background-color: @pushbutton_hover_bg;
-}
-
-QPushButton:flat {
-    border: none;
-}
-
-QPushButton:default {
-    border: 1px solid @pushbutton_default_border;
-}
-
-QPushButton:disabled {
-    color: @pushbutton_disabled_fg;
-    background-color: @pushbutton_disabled_bg;
-}
-
-QPushButton::menu-indicator {
-    image: url(arrow_dropdown.svg);
-    width: 16px;
-    height: 16px;
-}
-
 QPushButton[SpecialBtn="true"] {
     color: @pushbutton_specialbtn_fg;
     background: @pushbutton_specialbtn_bg;
 }
 
-QPushButton[SpecialBtn="true"]:focus {
-    background-color: @pushbutton_specialbtn_focus_bg;
+QPushButton[SpecialBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: @pushbutton_specialbtn_bg;
 }
 
-QPushButton[SpecialBtn="true"]:pressed {
-    background-color: @pushbutton_specialbtn_pressed_bg;
+QPushButton[SpecialBtn="true"]:focus {
+    background-color: @pushbutton_specialbtn_focus_bg;
 }
 
 QPushButton[SpecialBtn="true"]:checked {
@@ -255,16 +209,15 @@ QPushButton[SpecialBtn="true"]:hover {
     background-color: @pushbutton_specialbtn_hover_bg;
 }
 
+QPushButton[SpecialBtn="true"]:pressed {
+    background-color: @pushbutton_specialbtn_pressed_bg;
+}
+
 QPushButton[SpecialBtn="true"]:disabled {
     color: @pushbutton_disabled_fg;
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[SpecialBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: @pushbutton_specialbtn_bg;
-}
-
 QPushButton[CornerBtn="true"] {
     padding: 4px -2px 4px -2px;
     margin: 0px;
@@ -277,14 +230,19 @@ QPushButton[CornerBtn="true"]::menu-indicator {
     image: none;
 }
 
-QPushButton[CornerBtn="true"]:hover {
-    background-color: @pushbutton_cornerbtn_hover_bg;
+QPushButton[CornerBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: transparent;
 }
 
 QPushButton[CornerBtn="true"]:focus {
     background-color: @pushbutton_cornerbtn_focus_bg;
 }
 
+QPushButton[CornerBtn="true"]:hover {
+    background-color: @pushbutton_cornerbtn_hover_bg;
+}
+
 QPushButton[CornerBtn="true"]:pressed {
     background-color: @pushbutton_cornerbtn_pressed_bg;
 }
@@ -294,11 +252,6 @@ QPushButton[CornerBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[CornerBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: transparent;
-}
-
 QPushButton[StatusBtn="true"] {
     font: bold;
     padding: 0px 2px 0px 2px;
@@ -308,14 +261,19 @@ QPushButton[StatusBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[StatusBtn="true"]:hover {
-    background-color: @pushbutton_statusbtn_hover_bg;
+QPushButton[StatusBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: transparent;
 }
 
 QPushButton[StatusBtn="true"]:focus {
     background-color: @pushbutton_statusbtn_focus_bg;;
 }
 
+QPushButton[StatusBtn="true"]:hover {
+    background-color: @pushbutton_statusbtn_hover_bg;
+}
+
 QPushButton[StatusBtn="true"]:pressed {
     background-color: @pushbutton_statusbtn_pressed_bg;
 }
@@ -325,11 +283,6 @@ QPushButton[StatusBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[StatusBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: transparent;
-}
-
 QPushButton[FlatBtn="true"] {
     padding: 4px;
     margin: 0px;
@@ -338,14 +291,19 @@ QPushButton[FlatBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[FlatBtn="true"]:hover {
-    background-color: @pushbutton_flatbtn_hover_bg;
+QPushButton[FlatBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: transparent;
 }
 
 QPushButton[FlatBtn="true"]:focus {
     background-color: @pushbutton_flatbtn_focus_bg;
 }
 
+QPushButton[FlatBtn="true"]:hover {
+    background-color: @pushbutton_flatbtn_hover_bg;
+}
+
 QPushButton[FlatBtn="true"]:pressed {
     background-color: @pushbutton_flatbtn_pressed_bg;
 }
@@ -355,11 +313,6 @@ QPushButton[FlatBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[FlatBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: transparent;
-}
-
 QPushButton[SelectionBtn="true"] {
     padding: 4px 10px 4px 10px;
     border: none;
@@ -369,14 +322,19 @@ QPushButton[SelectionBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[SelectionBtn="true"]:hover {
-    background-color: @pushbutton_selectionbtn_hover_bg;
+QPushButton[SelectionBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: transparent;
 }
 
 QPushButton[SelectionBtn="true"]:focus {
     background-color: @pushbutton_selectionbtn_focus_bg;
 }
 
+QPushButton[SelectionBtn="true"]:hover {
+    background-color: @pushbutton_selectionbtn_hover_bg;
+}
+
 QPushButton[SelectionBtn="true"]:pressed {
     background-color: @pushbutton_selectionbtn_pressed_bg;
 }
@@ -386,11 +344,6 @@ QPushButton[SelectionBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[SelectionBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: transparent;
-}
-
 QPushButton[TitleBtn="true"] {
     padding: 4px;
     margin: 0px;
@@ -399,14 +352,19 @@ QPushButton[TitleBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[TitleBtn="true"]:hover {
-    background-color: @pushbutton_titlebtn_hover_bg;
+QPushButton[TitleBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: @pushbutton_titlebtn_bg;
 }
 
 QPushButton[TitleBtn="true"]:focus {
     background-color: @pushbutton_titlebtn_focus_bg;
 }
 
+QPushButton[TitleBtn="true"]:hover {
+    background-color: @pushbutton_titlebtn_hover_bg;
+}
+
 QPushButton[TitleBtn="true"]:pressed {
     background-color: @pushbutton_titlebtn_pressed_bg;
 }
@@ -416,11 +374,6 @@ QPushButton[TitleBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[TitleBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: @pushbutton_titlebtn_bg;
-}
-
 QPushButton[DangerBtn="true"] {
     color: @pushbutton_dangerbtn_fg;
     border: none;
@@ -428,21 +381,25 @@ QPushButton[DangerBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[DangerBtn="true"]:hover {
-    background-color: @pushbutton_dangerbtn_hover_bg;
+QPushButton[DangerBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: @pushbutton_dangerbtn_bg;
 }
 
 QPushButton[DangerBtn="true"]:focus {
     background-color: @pushbutton_dangerbtn_focus_bg;
 }
 
-QPushButton[DangerBtn="true"]:pressed {
+QPushButton[DangerBtn="true"]:checked {
     background-color: @pushbutton_dangerbtn_pressed_bg;
 }
 
-QPushButton[DangerBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: @pushbutton_dangerbtn_bg;
+QPushButton[DangerBtn="true"]:hover {
+    background-color: @pushbutton_dangerbtn_hover_bg;
+}
+
+QPushButton[DangerBtn="true"]:pressed {
+    background-color: @pushbutton_dangerbtn_pressed_bg;
 }
 
 QPushButton[DangerBtn="true"]:disabled {
@@ -459,28 +416,28 @@ QPushButton[ToolBoxActiveBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[ToolBoxActiveBtn="true"]:focus {
-    background-color: @pushbutton_toolboxbtn_active_focus_bg;
+QPushButton[ToolBoxActiveBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: @pushbutton_toolboxbtn_active_bg;
 }
 
-QPushButton[ToolBoxActiveBtn="true"]:hover {
-    background-color: @pushbutton_toolboxbtn_active_hover_bg;
+QPushButton[ToolBoxActiveBtn="true"]:focus {
+    background-color: @pushbutton_toolboxbtn_active_focus_bg;
 }
 
 QPushButton[ToolBoxActiveBtn="true"]:pressed {
     background-color: @pushbutton_toolboxbtn_active_pressed_bg;
 }
 
+QPushButton[ToolBoxActiveBtn="true"]:hover {
+    background-color: @pushbutton_toolboxbtn_active_hover_bg;
+}
+
 QPushButton[ToolBoxActiveBtn="true"]:disabled {
     color: @pushbutton_disabled_fg;
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[ToolBoxActiveBtn="true"]:default {
-    border: 1px solid @pushbutton_default_border;
-    background-color: @pushbutton_toolboxbtn_active_bg;
-}
-
 QPushButton[AvatarBtn="true"] {
     padding: 2px 4px 2px 4px;
     margin: 0px;
@@ -489,14 +446,19 @@ QPushButton[AvatarBtn="true"] {
     min-width: -1;
 }
 
-QPushButton[AvatarBtn="true"]:hover {
-    background-color: @pushbutton_avatarbtn_hover_bg;
-}
-
 QPushButton[AvatarBtn="true"]:focus {
     background-color: @pushbutton_avatarbtn_focus_bg;;
 }
 
+QPushButton[AvatarBtn="true"]:default {
+    border: 1px solid @pushbutton_default_border;
+    background-color: transparent;
+}
+
+QPushButton[AvatarBtn="true"]:hover {
+    background-color: @pushbutton_avatarbtn_hover_bg;
+}
+
 QPushButton[AvatarBtn="true"]:pressed {
     background-color: @pushbutton_avatarbtn_pressed_bg;
 }
@@ -506,9 +468,51 @@ QPushButton[AvatarBtn="true"]:disabled {
     background-color: @pushbutton_disabled_bg;
 }
 
-QPushButton[AvatarBtn="true"]:default {
+QPushButton {
+    color: @pushbutton_fg;
+    background: @pushbutton_bg;
+    border: 1px solid @pushbutton_border;
+    padding: 3px;
+    min-width: 80px;
+}
+
+QPushButton:focus {
+    background-color: @pushbutton_focus_bg;
+}
+
+QPushButton:checked {
+    background-color: @pushbutton_checked_bg;
+}
+
+QPushButton:checked:hover {
+    background-color: @pushbutton_hover_bg;
+}
+
+QPushButton:flat {
+    border: none;
+}
+
+QPushButton:default {
     border: 1px solid @pushbutton_default_border;
-    background-color: transparent;
+}
+
+QPushButton:hover {
+    background-color: @pushbutton_hover_bg;
+}
+
+QPushButton:pressed {
+    background-color: @pushbutton_pressed_bg;
+}
+
+QPushButton:disabled {
+    color: @pushbutton_disabled_fg;
+    background-color: @pushbutton_disabled_bg;
+}
+
+QPushButton::menu-indicator {
+    image: url(arrow_dropdown.svg);
+    width: 16px;
+    height: 16px;
 }
 
 VButtonMenuItem {
@@ -544,11 +548,11 @@ VButtonMenuItem[Heading6="true"] {
     font-size: 14pt;
 }
 
-VButtonMenuItem:hover {
+VButtonMenuItem:focus {
     background-color: @menubar_item_selected_bg;
 }
 
-VButtonMenuItem:focus {
+VButtonMenuItem:hover {
     background-color: @menubar_item_selected_bg;
 }
 

+ 11 - 0
src/resources/vnote.ini

@@ -149,6 +149,9 @@ enable_compact_mode=true
 ; Whether enable tools dock widget
 tools_dock_checked=true
 
+; Whether enable search dock widget
+search_dock_checked=false
+
 ; Whether show menu bar
 menu_bar_checked=true
 
@@ -201,6 +204,10 @@ single_click_close_previous_tab=true
 ; Whether enable auto wildcard match in simple search like list and tree widgets
 enable_wildcard_in_simple_search=true
 
+; Search options
+; scope,object,target,engine,option,pattern
+search_options=4,2,7,0,0,""
+
 [export]
 ; Path of the wkhtmltopdf tool
 wkhtmltopdf=wkhtmltopdf
@@ -279,6 +286,8 @@ Find=Ctrl+F
 FindNext=F3
 ; Find previous occurence
 FindPrevious=Shift+F3
+; Advanced find
+AdvancedFind=Ctrl+Alt+F
 ; Recover last closed file
 LastClosedFile=Ctrl+Shift+T
 ; Activate next tab
@@ -311,6 +320,8 @@ OnePanelView=P
 DiscardAndRead=Q
 ; Toggle Tools dock widget
 ToolsDock=T
+; Toggle Search dock widget
+SearchDock=C
 ; Close current note
 CloseNote=X
 ; Show shortcuts help document

+ 12 - 2
src/src.pro

@@ -113,7 +113,11 @@ SOURCES += main.cpp\
     vstyleditemdelegate.cpp \
     vtreewidget.cpp \
     dialog/vexportdialog.cpp \
-    vexporter.cpp
+    vexporter.cpp \
+    vsearcher.cpp \
+    vsearch.cpp \
+    vsearchresulttree.cpp \
+    vsearchengine.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -214,7 +218,13 @@ HEADERS  += vmainwindow.h \
     vtreewidget.h \
     dialog/vexportdialog.h \
     vexporter.h \
-    vwordcountinfo.h
+    vwordcountinfo.h \
+    vsearcher.h \
+    vsearch.h \
+    vsearchresulttree.h \
+    isearchengine.h \
+    vsearchconfig.h \
+    vsearchengine.h
 
 RESOURCES += \
     vnote.qrc \

+ 11 - 6
src/utils/vutils.cpp

@@ -522,13 +522,18 @@ bool VUtils::isImageURLText(const QString &p_url)
 
 qreal VUtils::calculateScaleFactor()
 {
-    // const qreal refHeight = 1152;
-    // const qreal refWidth = 2048;
-    const qreal refDpi = 96;
+    static qreal factor = -1;
+
+    if (factor < 0) {
+        const qreal refDpi = 96;
+        qreal dpi = QGuiApplication::primaryScreen()->logicalDotsPerInch();
+        factor = dpi / refDpi;
+        if (factor < 1) {
+            factor = 1;
+        }
+    }
 
-    qreal dpi = QGuiApplication::primaryScreen()->logicalDotsPerInch();
-    qreal factor = dpi / refDpi;
-    return factor < 1 ? 1 : factor;
+    return factor;
 }
 
 bool VUtils::realEqual(qreal p_a, qreal p_b)

+ 1 - 4
src/vconfigmanager.cpp

@@ -104,8 +104,6 @@ void VConfigManager::initialize()
     curRenderBackgroundColor = getConfigFromSettings("global",
                                                      "current_render_background_color").toString();
 
-    m_toolsDockChecked = getConfigFromSettings("global", "tools_dock_checked").toBool();
-
     m_findCaseSensitive = getConfigFromSettings("global",
                                                 "find_case_sensitive").toBool();
     m_findWholeWordOnly = getConfigFromSettings("global",
@@ -498,11 +496,9 @@ QString VConfigManager::fetchDirConfigFilePath(const QString &p_path)
         if (!dir.rename(c_obsoleteDirConfigFile, c_dirConfigFile)) {
             fileName = c_obsoleteDirConfigFile;
         }
-        qDebug() << "rename old directory config file:" << fileName;
     }
 
     QString filePath = QDir::cleanPath(dir.filePath(fileName));
-    qDebug() << "use directory config file:" << filePath;
     return filePath;
 }
 
@@ -1473,6 +1469,7 @@ void VConfigManager::resetConfigurations()
 void VConfigManager::resetLayoutConfigurations()
 {
     resetDefaultConfig("global", "tools_dock_checked");
+    resetDefaultConfig("global", "search_dock_checked");
     resetDefaultConfig("global", "menu_bar_checked");
     resetDefaultConfig("global", "enable_compact_mode");
 

+ 33 - 6
src/vconfigmanager.h

@@ -184,6 +184,9 @@ public:
     bool getToolsDockChecked() const;
     void setToolsDockChecked(bool p_checked);
 
+    bool getSearchDockChecked() const;
+    void setSearchDockChecked(bool p_checked);
+
     const QByteArray &getMainWindowGeometry() const;
     void setMainWindowGeometry(const QByteArray &p_geometry);
 
@@ -466,6 +469,9 @@ public:
     QStringList getCustomExport() const;
     void setCustomExport(const QStringList &p_exp);
 
+    QStringList getSearchOptions() const;
+    void setSearchOptions(const QStringList &p_opts);
+
 private:
     // Look up a config from user and default settings.
     QVariant getConfigFromSettings(const QString &section, const QString &key) const;
@@ -605,8 +611,6 @@ private:
 
     QString curRenderBackgroundColor;
 
-    bool m_toolsDockChecked;
-
     QByteArray m_mainWindowGeometry;
     QByteArray m_mainWindowState;
     QByteArray m_mainSplitterState;
@@ -1153,14 +1157,26 @@ inline void VConfigManager::setCurRenderBackgroundColor(const QString &colorName
 
 inline bool VConfigManager::getToolsDockChecked() const
 {
-    return m_toolsDockChecked;
+    return getConfigFromSettings("global", "tools_dock_checked").toBool();
 }
 
 inline void VConfigManager::setToolsDockChecked(bool p_checked)
 {
-    m_toolsDockChecked = p_checked;
-    setConfigToSettings("global", "tools_dock_checked",
-                        m_toolsDockChecked);
+    setConfigToSettings("global",
+                        "tools_dock_checked",
+                        p_checked);
+}
+
+inline bool VConfigManager::getSearchDockChecked() const
+{
+    return getConfigFromSettings("global", "search_dock_checked").toBool();
+}
+
+inline void VConfigManager::setSearchDockChecked(bool p_checked)
+{
+    setConfigToSettings("global",
+                        "search_dock_checked",
+                        p_checked);
 }
 
 inline const QByteArray& VConfigManager::getMainWindowGeometry() const
@@ -2146,4 +2162,15 @@ inline void VConfigManager::setCustomExport(const QStringList &p_exp)
 {
     setConfigToSettings("export", "custom_export", p_exp);
 }
+
+inline QStringList VConfigManager::getSearchOptions() const
+{
+    return getConfigFromSettings("global",
+                                 "search_options").toStringList();
+}
+
+inline void VConfigManager::setSearchOptions(const QStringList &p_opts)
+{
+    setConfigToSettings("global", "search_options", p_opts);
+}
 #endif // VCONFIGMANAGER_H

+ 3 - 1
src/vdirectorytree.cpp

@@ -191,12 +191,14 @@ void VDirectoryTree::setNotebook(VNotebook *p_notebook)
 
 void VDirectoryTree::fillTreeItem(QTreeWidgetItem *p_item, VDirectory *p_directory)
 {
+    static QIcon itemIcon = VIconUtils::treeViewIcon(":/resources/icons/dir_item.svg");
+
     int col = 0;
     QString name = p_directory->getName();
     p_item->setText(col, name);
     p_item->setToolTip(col, name);
     p_item->setData(col, Qt::UserRole, QVariant::fromValue(p_directory));
-    p_item->setIcon(col, VIconUtils::treeViewIcon(":/resources/icons/dir_item.svg"));
+    p_item->setIcon(col, itemIcon);
 }
 
 void VDirectoryTree::updateDirectoryTree()

+ 1 - 0
src/vedit.cpp

@@ -188,6 +188,7 @@ void VEdit::insertLink()
                              "",
                              linkText,
                              linkUrl,
+                             false,
                              this);
     if (dialog.exec() == QDialog::Accepted) {
         linkText = dialog.getLinkText();

+ 14 - 0
src/veditarea.cpp

@@ -570,6 +570,20 @@ VEditTab *VEditArea::getTab(int p_winIdx, int p_tabIdx) const
     return win->getTab(p_tabIdx);
 }
 
+VEditTab *VEditArea::getTab(const VFile *p_file) const
+{
+    int nrWin = splitter->count();
+    for (int winIdx = 0; winIdx < nrWin; ++winIdx) {
+        VEditWindow *win = getWindow(winIdx);
+        int tabIdx = win->findTabByFile(p_file);
+        if (tabIdx != -1) {
+            return win->getTab(tabIdx);
+        }
+    }
+
+    return NULL;
+}
+
 QVector<VEditTabInfo> VEditArea::getAllTabsInfo() const
 {
     QVector<VEditTabInfo> tabs;

+ 3 - 0
src/veditarea.h

@@ -44,6 +44,9 @@ public:
     // Return the @p_tabIdx tab in the @p_winIdx window.
     VEditTab *getTab(int p_winIdx, int p_tabIdx) const;
 
+    // Return the tab for @p_file file.
+    VEditTab *getTab(const VFile *p_file) const;
+
     // Return VEditTabInfo of all edit tabs.
     QVector<VEditTabInfo> getAllTabsInfo() const;
 

+ 104 - 9
src/vmainwindow.cpp

@@ -38,6 +38,7 @@
 #include "dialog/vtipsdialog.h"
 #include "vcart.h"
 #include "dialog/vexportdialog.h"
+#include "vsearcher.h"
 
 extern VConfigManager *g_config;
 
@@ -148,6 +149,7 @@ void VMainWindow::registerCaptainAndNavigationTargets()
     m_captain->registerNavigationTarget(m_toolBox);
     m_captain->registerNavigationTarget(outline);
     m_captain->registerNavigationTarget(m_snippetList);
+    m_captain->registerNavigationTarget(m_searcher);
 
     // Register Captain mode targets.
     m_captain->registerCaptainTarget(tr("AttachmentList"),
@@ -174,6 +176,10 @@ void VMainWindow::registerCaptainAndNavigationTargets()
                                      g_config->getCaptainShortcutKeySequence("ToolsDock"),
                                      this,
                                      toggleToolsDockByCaptain);
+    m_captain->registerCaptainTarget(tr("SearchDock"),
+                                     g_config->getCaptainShortcutKeySequence("SearchDock"),
+                                     this,
+                                     toggleSearchDockByCaptain);
     m_captain->registerCaptainTarget(tr("CloseNote"),
                                      g_config->getCaptainShortcutKeySequence("CloseNote"),
                                      this,
@@ -1073,6 +1079,17 @@ void VMainWindow::initEditMenu()
     connect(m_findReplaceAct, &QAction::triggered,
             this, &VMainWindow::openFindDialog);
 
+    QAction *advFindAct = new QAction(tr("Advanced Find"), this);
+    advFindAct->setToolTip(tr("Advanced find within VNote"));
+    keySeq = g_config->getShortcutKeySequence("AdvancedFind");
+    qDebug() << "set AdvancedFind shortcut to" << keySeq;
+    advFindAct->setShortcut(QKeySequence(keySeq));
+    connect(advFindAct, &QAction::triggered,
+            this, [this]() {
+                m_searchDock->setVisible(true);
+                m_searcher->focusToSearch();
+            });
+
     m_findNextAct = new QAction(tr("Find Next"), this);
     m_findNextAct->setToolTip(tr("Find next occurence"));
     keySeq = g_config->getShortcutKeySequence("FindNext");
@@ -1188,6 +1205,8 @@ void VMainWindow::initEditMenu()
     QMenu *findReplaceMenu = editMenu->addMenu(tr("Find/Replace"));
     findReplaceMenu->setToolTipsVisible(true);
     findReplaceMenu->addAction(m_findReplaceAct);
+    findReplaceMenu->addAction(advFindAct);
+    findReplaceMenu->addSeparator();
     findReplaceMenu->addAction(m_findNextAct);
     findReplaceMenu->addAction(m_findPreviousAct);
     findReplaceMenu->addAction(m_replaceAct);
@@ -1268,9 +1287,22 @@ void VMainWindow::initEditMenu()
 
 void VMainWindow::initDockWindows()
 {
-    toolDock = new QDockWidget(tr("Tools"), this);
-    toolDock->setObjectName("ToolsDock");
-    toolDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
+    setTabPosition(Qt::LeftDockWidgetArea, QTabWidget::West);
+    setTabPosition(Qt::RightDockWidgetArea, QTabWidget::East);
+    setTabPosition(Qt::TopDockWidgetArea, QTabWidget::North);
+    setTabPosition(Qt::BottomDockWidgetArea, QTabWidget::South);
+
+    setDockNestingEnabled(true);
+
+    initToolsDock();
+    initSearchDock();
+}
+
+void VMainWindow::initToolsDock()
+{
+    m_toolDock = new QDockWidget(tr("Tools"), this);
+    m_toolDock->setObjectName("ToolsDock");
+    m_toolDock->setAllowedAreas(Qt::AllDockWidgetAreas);
 
     // Outline tree.
     outline = new VOutline(this);
@@ -1298,16 +1330,35 @@ void VMainWindow::initDockWindows()
                        ":/resources/icons/cart.svg",
                        tr("Cart"));
 
-    toolDock->setWidget(m_toolBox);
-    addDockWidget(Qt::RightDockWidgetArea, toolDock);
+    m_toolDock->setWidget(m_toolBox);
+    addDockWidget(Qt::RightDockWidgetArea, m_toolDock);
 
-    QAction *toggleAct = toolDock->toggleViewAction();
+    QAction *toggleAct = m_toolDock->toggleViewAction();
     toggleAct->setToolTip(tr("Toggle the tools dock widget"));
     VUtils::fixTextWithCaptainShortcut(toggleAct, "ToolsDock");
 
     m_viewMenu->addAction(toggleAct);
 }
 
+void VMainWindow::initSearchDock()
+{
+    m_searchDock = new QDockWidget(tr("Search"), this);
+    m_searchDock->setObjectName("SearchDock");
+    m_searchDock->setAllowedAreas(Qt::AllDockWidgetAreas);
+
+    m_searcher = new VSearcher(this);
+
+    m_searchDock->setWidget(m_searcher);
+
+    addDockWidget(Qt::RightDockWidgetArea, m_searchDock);
+
+    QAction *toggleAct = m_searchDock->toggleViewAction();
+    toggleAct->setToolTip(tr("Toggle the search dock widget"));
+    VUtils::fixTextWithCaptainShortcut(toggleAct, "SearchDock");
+
+    m_viewMenu->addAction(toggleAct);
+}
+
 void VMainWindow::importNoteFromFile()
 {
     static QString lastPath = QDir::homePath();
@@ -2162,7 +2213,8 @@ void VMainWindow::saveStateAndGeometry()
 {
     g_config->setMainWindowGeometry(saveGeometry());
     g_config->setMainWindowState(saveState());
-    g_config->setToolsDockChecked(toolDock->isVisible());
+    g_config->setToolsDockChecked(m_toolDock->isVisible());
+    g_config->setSearchDockChecked(m_searchDock->isVisible());
 
     if (m_panelViewState == PanelViewState::CompactMode) {
         g_config->setNaviSplitterState(m_naviSplitter->saveState());
@@ -2181,11 +2233,14 @@ void VMainWindow::restoreStateAndGeometry()
     if (!geometry.isEmpty()) {
         restoreGeometry(geometry);
     }
+
     const QByteArray &state = g_config->getMainWindowState();
     if (!state.isEmpty()) {
         restoreState(state);
     }
-    toolDock->setVisible(g_config->getToolsDockChecked());
+
+    m_toolDock->setVisible(g_config->getToolsDockChecked());
+    m_searchDock->setVisible(g_config->getSearchDockChecked());
 
     const QByteArray &splitterState = g_config->getMainSplitterState();
     if (!splitterState.isEmpty()) {
@@ -2260,6 +2315,32 @@ bool VMainWindow::locateFile(VFile *p_file)
     return ret;
 }
 
+bool VMainWindow::locateDirectory(VDirectory *p_directory)
+{
+    bool ret = false;
+    if (!p_directory) {
+        return ret;
+    }
+
+    VNotebook *notebook = p_directory->getNotebook();
+    if (notebookSelector->locateNotebook(notebook)) {
+        while (directoryTree->currentNotebook() != notebook) {
+            QCoreApplication::sendPostedEvents();
+        }
+
+        ret = directoryTree->locateDirectory(p_directory);
+    }
+
+    // Open the directory and file panels after location.
+    if (m_panelViewState == PanelViewState::CompactMode) {
+        compactModeView();
+    } else {
+        twoPanelView();
+    }
+
+    return ret;
+}
+
 void VMainWindow::handleFindDialogTextChanged(const QString &p_text, uint /* p_options */)
 {
     bool enabled = true;
@@ -2659,7 +2740,21 @@ bool VMainWindow::toggleToolsDockByCaptain(void *p_target, void *p_data)
 {
     Q_UNUSED(p_data);
     VMainWindow *obj = static_cast<VMainWindow *>(p_target);
-    obj->toolDock->setVisible(!obj->toolDock->isVisible());
+    obj->m_toolDock->setVisible(!obj->m_toolDock->isVisible());
+    return true;
+}
+
+bool VMainWindow::toggleSearchDockByCaptain(void *p_target, void *p_data)
+{
+    Q_UNUSED(p_data);
+    VMainWindow *obj = static_cast<VMainWindow *>(p_target);
+    bool visible = obj->m_searchDock->isVisible();
+    obj->m_searchDock->setVisible(!visible);
+    if (!visible) {
+        obj->m_searcher->focusToSearch();
+        return false;
+    }
+
     return true;
 }
 

+ 29 - 1
src/vmainwindow.h

@@ -39,6 +39,7 @@ class VButtonWithWidget;
 class VAttachmentList;
 class VSnippetList;
 class VCart;
+class VSearcher;
 class QPrinter;
 
 enum class PanelViewState
@@ -60,6 +61,9 @@ public:
     // Returns true if the location succeeds.
     bool locateFile(VFile *p_file);
 
+    // Returns true if the location succeeds.
+    bool locateDirectory(VDirectory *p_directory);
+
     VFileList *getFileList() const;
 
     VEditArea *getEditArea() const;
@@ -68,6 +72,10 @@ public:
 
     VCart *getCart() const;
 
+    VDirectoryTree *getDirectoryTree() const;
+
+    VNotebookSelector *getNotebookSelector() const;
+
     // View and edit the information of @p_file, which is an orphan file.
     void editOrphanFileInfo(VFile *p_file);
 
@@ -205,6 +213,10 @@ private:
 
     void initDockWindows();
 
+    void initToolsDock();
+
+    void initSearchDock();
+
     void initRenderBackgroundMenu(QMenu *menu);
 
     void initRenderStyleMenu(QMenu *p_menu);
@@ -280,6 +292,8 @@ private:
 
     static bool toggleToolsDockByCaptain(void *p_target, void *p_data);
 
+    static bool toggleSearchDockByCaptain(void *p_target, void *p_data);
+
     static bool closeFileByCaptain(void *p_target, void *p_data);
 
     static bool shortcutsHelpByCaptain(void *p_target, void *p_data);
@@ -311,7 +325,9 @@ private:
 
     VEditArea *editArea;
 
-    QDockWidget *toolDock;
+    QDockWidget *m_toolDock;
+
+    QDockWidget *m_searchDock;
 
     // Tool box in the dock widget.
     VToolBox *m_toolBox;
@@ -324,6 +340,9 @@ private:
     // View and manage cart.
     VCart *m_cart;
 
+    // Advanced search.
+    VSearcher *m_searcher;
+
     VFindReplaceDialog *m_findReplaceDialog;
 
     VVimCmdLineEdit *m_vimCmd;
@@ -453,4 +472,13 @@ inline VCart *VMainWindow::getCart() const
     return m_cart;
 }
 
+inline VDirectoryTree *VMainWindow::getDirectoryTree() const
+{
+    return directoryTree;
+}
+
+inline VNotebookSelector *VMainWindow::getNotebookSelector() const
+{
+    return notebookSelector;
+}
 #endif // VMAINWINDOW_H

+ 2 - 0
src/vmdeditor.cpp

@@ -95,6 +95,8 @@ VMdEditor::VMdEditor(VFile *p_file,
     connect(this, &VTextEdit::cursorPositionChanged,
             this, &VMdEditor::updateCurrentHeader);
 
+    setDisplayScaleFactor(VUtils::calculateScaleFactor());
+
     updateFontAndPalette();
 
     updateConfig();

+ 11 - 0
src/vnote.cpp

@@ -314,6 +314,17 @@ VDirectory *VNote::getInternalDirectory(const QString &p_path)
 
 }
 
+VNotebook *VNote::getNotebook(const QString &p_path)
+{
+    for (auto & nb : m_notebooks) {
+        if (VUtils::equalPath(nb->getPath(), p_path)) {
+            return nb;
+        }
+    }
+
+    return NULL;
+}
+
 void VNote::freeOrphanFiles()
 {
     for (int i = 0; i < m_externalFiles.size();) {

+ 4 - 0
src/vnote.h

@@ -95,6 +95,10 @@ public:
     // Otherwise, returns NULL.
     VDirectory *getInternalDirectory(const QString &p_path);
 
+    // Given the path of a file, try to find it in all notebooks.
+    // Returns a VNotebook struct if it is the root folder of a notebook.
+    VNotebook *getNotebook(const QString &p_path);
+
     void freeOrphanFiles();
 
     // @p_renderBg: background color, empty to not specify given color.

+ 5 - 0
src/vnote.qrc

@@ -243,5 +243,10 @@
         <file>resources/themes/v_moonlight/arrow_dropdown_disabled.svg</file>
         <file>resources/themes/v_pure/arrow_dropdown_disabled.svg</file>
         <file>resources/themes/v_white/arrow_dropdown_disabled.svg</file>
+        <file>resources/icons/clear_search.svg</file>
+        <file>resources/icons/search.svg</file>
+        <file>resources/icons/search_advanced.svg</file>
+        <file>resources/icons/search_console.svg</file>
+        <file>resources/icons/note_item.svg</file>
     </qresource>
 </RCC>

+ 0 - 1
src/vopenedlistmenu.cpp

@@ -7,7 +7,6 @@
 #include <QString>
 #include <QStyleFactory>
 #include <QWidgetAction>
-#include <QLabel>
 
 #include "veditwindow.h"
 #include "vnotefile.h"

+ 418 - 0
src/vsearch.cpp

@@ -0,0 +1,418 @@
+#include "vsearch.h"
+
+#include "utils/vutils.h"
+#include "vfile.h"
+#include "vdirectory.h"
+#include "vnotebook.h"
+#include "veditarea.h"
+#include "vmainwindow.h"
+#include "vtableofcontent.h"
+#include "vsearchengine.h"
+
+extern VMainWindow *g_mainWin;
+
+VSearch::VSearch(QObject *p_parent)
+    : QObject(p_parent),
+      m_askedToStop(false),
+      m_engine(NULL)
+{
+}
+
+QSharedPointer<VSearchResult> VSearch::search(const QVector<VFile *> &p_files)
+{
+    Q_ASSERT(!askedToStop());
+
+    QSharedPointer<VSearchResult> result(new VSearchResult(this));
+
+    if (p_files.isEmpty()) {
+        return result;
+    }
+
+    if (!testTarget(VSearchConfig::Note)) {
+        qDebug() << "search is not applicable for note";
+        result->m_state = VSearchState::Success;
+        return result;
+    }
+
+    result->m_state = VSearchState::Busy;
+
+    for (auto const & it : p_files) {
+        if (!it) {
+            continue;
+        }
+
+        searchFirstPhase(it, result, true);
+
+        if (askedToStop()) {
+            qDebug() << "asked to cancel the search";
+            result->m_state = VSearchState::Cancelled;
+            break;
+        }
+    }
+
+    if (result->m_state == VSearchState::Busy) {
+        result->m_state = VSearchState::Success;
+    }
+
+    return result;
+}
+
+QSharedPointer<VSearchResult> VSearch::search(VDirectory *p_directory)
+{
+    Q_ASSERT(!askedToStop());
+
+    QSharedPointer<VSearchResult> result(new VSearchResult(this));
+
+    if (!p_directory) {
+        return result;
+    }
+
+    if ((!testTarget(VSearchConfig::Note)
+         && !testTarget(VSearchConfig::Folder))
+        || testObject(VSearchConfig::Outline)) {
+        qDebug() << "search is not applicable for folder";
+        result->m_state = VSearchState::Success;
+        return result;
+    }
+
+    result->m_state = VSearchState::Busy;
+
+    searchFirstPhase(p_directory, result);
+
+    if (result->hasSecondPhaseItems()) {
+        searchSecondPhase(result);
+    } else if (result->m_state == VSearchState::Busy) {
+        result->m_state = VSearchState::Success;
+    }
+
+    return result;
+}
+
+QSharedPointer<VSearchResult> VSearch::search(const QVector<VNotebook *> &p_notebooks)
+{
+    Q_ASSERT(!askedToStop());
+
+    QSharedPointer<VSearchResult> result(new VSearchResult(this));
+
+    if (p_notebooks.isEmpty()) {
+        return result;
+    }
+
+    if (testObject(VSearchConfig::Outline)) {
+        qDebug() << "search is not applicable for notebook";
+        result->m_state = VSearchState::Success;
+        return result;
+    }
+
+    result->m_state = VSearchState::Busy;
+
+    for (auto const & nb : p_notebooks) {
+        if (!nb) {
+            continue;
+        }
+
+        searchFirstPhase(nb, result);
+
+        if (askedToStop()) {
+            qDebug() << "asked to cancel the search";
+            result->m_state = VSearchState::Cancelled;
+            break;
+        }
+    }
+
+    if (result->hasSecondPhaseItems()) {
+        searchSecondPhase(result);
+    } else if (result->m_state == VSearchState::Busy) {
+        result->m_state = VSearchState::Success;
+    }
+
+    return result;
+}
+
+void VSearch::searchFirstPhase(VFile *p_file,
+                               const QSharedPointer<VSearchResult> &p_result,
+                               bool p_searchContent)
+{
+    Q_ASSERT(testTarget(VSearchConfig::Note));
+
+    QString name = p_file->getName();
+    if (!m_patternReg.isEmpty()) {
+        if (!matchOneLine(name, m_patternReg)) {
+            return;
+        }
+    }
+
+    QString filePath = p_file->fetchPath();
+    if (testObject(VSearchConfig::Name)) {
+        if (matchOneLine(name, m_searchReg)) {
+            VSearchResultItem *item = new VSearchResultItem(VSearchResultItem::Note,
+                                                            VSearchResultItem::LineNumber,
+                                                            name,
+                                                            filePath);
+            QSharedPointer<VSearchResultItem> pitem(item);
+            emit resultItemAdded(pitem);
+        }
+    }
+
+    if (testObject(VSearchConfig::Outline)) {
+        VSearchResultItem *item = searchForOutline(p_file);
+        if (item) {
+            QSharedPointer<VSearchResultItem> pitem(item);
+            emit resultItemAdded(pitem);
+        }
+    }
+
+    if (testObject(VSearchConfig::Content)) {
+        // Search content in first phase.
+        if (p_searchContent) {
+            VSearchResultItem *item = searchForContent(p_file);
+            if (item) {
+                QSharedPointer<VSearchResultItem> pitem(item);
+                emit resultItemAdded(pitem);
+            }
+        } else {
+            // Add an item for second phase process.
+            p_result->addSecondPhaseItem(filePath);
+        }
+    }
+}
+
+void VSearch::searchFirstPhase(VDirectory *p_directory,
+                               const QSharedPointer<VSearchResult> &p_result)
+{
+    if (!testTarget(VSearchConfig::Note)
+        && !testTarget(VSearchConfig::Folder)) {
+        return;
+    }
+
+    bool opened = p_directory->isOpened();
+    if (!opened && !p_directory->open()) {
+        p_result->logError(QString("Fail to open folder %1.").arg(p_directory->fetchRelativePath()));
+        p_result->m_state = VSearchState::Fail;
+        return;
+    }
+
+    if (testTarget(VSearchConfig::Folder)
+        && testObject(VSearchConfig::Name)) {
+        QString text = p_directory->getName();
+        if (matchOneLine(text, m_searchReg)) {
+            VSearchResultItem *item = new VSearchResultItem(VSearchResultItem::Folder,
+                                                            VSearchResultItem::LineNumber,
+                                                            text,
+                                                            p_directory->fetchPath());
+            QSharedPointer<VSearchResultItem> pitem(item);
+            emit resultItemAdded(pitem);
+        }
+    }
+
+    // Search files.
+    if (testTarget(VSearchConfig::Note)) {
+        for (auto const & file : p_directory->getFiles()) {
+            if (askedToStop()) {
+                qDebug() << "asked to cancel the search";
+                p_result->m_state = VSearchState::Cancelled;
+                goto exit;
+            }
+
+            searchFirstPhase(file, p_result);
+        }
+    }
+
+    // Search subfolders.
+    for (auto const & dir : p_directory->getSubDirs()) {
+        if (askedToStop()) {
+            qDebug() << "asked to cancel the search";
+            p_result->m_state = VSearchState::Cancelled;
+            goto exit;
+        }
+
+        searchFirstPhase(dir, p_result);
+    }
+
+exit:
+    if (!opened) {
+        p_directory->close();
+    }
+}
+
+void VSearch::searchFirstPhase(VNotebook *p_notebook,
+                               const QSharedPointer<VSearchResult> &p_result)
+{
+    bool opened = p_notebook->isOpened();
+    if (!opened && !p_notebook->open()) {
+        p_result->logError(QString("Fail to open notebook %1.").arg(p_notebook->getName()));
+        p_result->m_state = VSearchState::Fail;
+        return;
+    }
+
+    if (testTarget(VSearchConfig::Notebook)
+        && testObject(VSearchConfig::Name)) {
+        QString text = p_notebook->getName();
+        if (matchOneLine(text, m_searchReg)) {
+            VSearchResultItem *item = new VSearchResultItem(VSearchResultItem::Notebook,
+                                                            VSearchResultItem::LineNumber,
+                                                            text,
+                                                            p_notebook->getPath());
+            QSharedPointer<VSearchResultItem> pitem(item);
+            emit resultItemAdded(pitem);
+        }
+    }
+
+    if (!testTarget(VSearchConfig::Note)
+        && !testTarget(VSearchConfig::Folder)) {
+        goto exit;
+    }
+
+    // Search for subfolders.
+    for (auto const & dir : p_notebook->getRootDir()->getSubDirs()) {
+        if (askedToStop()) {
+            qDebug() << "asked to cancel the search";
+            p_result->m_state = VSearchState::Cancelled;
+            goto exit;
+        }
+
+        searchFirstPhase(dir, p_result);
+    }
+
+exit:
+    if (!opened) {
+        p_notebook->close();
+    }
+}
+
+VSearchResultItem *VSearch::searchForOutline(const VFile *p_file) const
+{
+    VEditTab *tab = g_mainWin->getEditArea()->getTab(p_file);
+    if (!tab) {
+        return NULL;
+    }
+
+    const VTableOfContent &toc = tab->getOutline();
+    const QVector<VTableOfContentItem> &table = toc.getTable();
+    VSearchResultItem *item = NULL;
+    for (auto const & it: table) {
+        if (it.isEmpty()) {
+            continue;
+        }
+
+        if (!matchOneLine(it.m_name, m_searchReg)) {
+            continue;
+        }
+
+        if (!item) {
+            item = new VSearchResultItem(VSearchResultItem::Note,
+                                         VSearchResultItem::OutlineIndex,
+                                         p_file->getName(),
+                                         p_file->fetchPath());
+        }
+
+        VSearchResultSubItem sitem(it.m_index, it.m_name);
+        item->m_matches.append(sitem);
+    }
+
+    return item;
+}
+
+VSearchResultItem *VSearch::searchForContent(const VFile *p_file) const
+{
+    Q_ASSERT(p_file->isOpened());
+    const QString &content = p_file->getContent();
+    if (content.isEmpty()) {
+        return NULL;
+    }
+
+    VSearchResultItem *item = NULL;
+    int lineNum = 1;
+    int pos = 0;
+    int size = content.size();
+    QRegExp newLineReg = QRegExp("\\n|\\r\\n|\\r");
+    Qt::CaseSensitivity cs = testOption(VSearchConfig::CaseSensitive)
+                             ? Qt::CaseSensitive : Qt::CaseInsensitive;
+    while (pos < size) {
+        int idx = content.indexOf(newLineReg, pos);
+        if (idx == -1) {
+            idx = size;
+        }
+
+        if (idx > pos) {
+            QString lineText = content.mid(pos, idx - pos);
+            bool matched = false;
+            if (m_contentSearchReg.isEmpty()) {
+                matched = lineText.contains(m_config->m_keyword, cs);
+            } else {
+                matched = (m_contentSearchReg.indexIn(lineText) != -1);
+            }
+
+            if (matched) {
+                if (!item) {
+                    item = new VSearchResultItem(VSearchResultItem::Note,
+                                                 VSearchResultItem::LineNumber,
+                                                 p_file->getName(),
+                                                 p_file->fetchPath());
+                }
+
+                VSearchResultSubItem sitem(lineNum, lineText);
+                item->m_matches.append(sitem);
+            }
+        }
+
+        if (idx == size) {
+            break;
+        }
+
+        pos = idx + newLineReg.matchedLength();
+        ++lineNum;
+    }
+
+    return item;
+}
+
+void VSearch::searchSecondPhase(const QSharedPointer<VSearchResult> &p_result)
+{
+    delete m_engine;
+    m_engine = NULL;
+
+    switch (m_config->m_engine) {
+    case VSearchConfig::Internal:
+    {
+        m_engine = new VSearchEngine(this);
+        m_engine->search(m_config, p_result);
+        break;
+    }
+
+    default:
+        p_result->m_state = VSearchState::Success;
+        break;
+    }
+
+    if (m_engine) {
+        connect(m_engine, &ISearchEngine::finished,
+                this, &VSearch::finished);
+        connect(m_engine, &ISearchEngine::resultItemAdded,
+                this, &VSearch::resultItemAdded);
+    }
+}
+
+void VSearch::clear()
+{
+    m_config.clear();
+
+    if (m_engine) {
+        m_engine->clear();
+
+        delete m_engine;
+        m_engine = NULL;
+    }
+
+    m_askedToStop = false;
+}
+
+void VSearch::stop()
+{
+    qDebug() << "VSearch asked to stop";
+    m_askedToStop = true;
+
+    if (m_engine) {
+        m_engine->stop();
+    }
+}

+ 162 - 0
src/vsearch.h

@@ -0,0 +1,162 @@
+#ifndef VSEARCH_H
+#define VSEARCH_H
+
+#include <QObject>
+#include <QString>
+#include <QSharedPointer>
+#include <QRegExp>
+#include <QCoreApplication>
+
+#include "vsearchconfig.h"
+
+class VFile;
+class VDirectory;
+class VNotebook;
+class ISearchEngine;
+
+
+class VSearch : public QObject
+{
+    Q_OBJECT
+public:
+    explicit VSearch(QObject *p_parent = nullptr);
+
+    void setConfig(QSharedPointer<VSearchConfig> p_config);
+
+    // Search list of files for CurrentNote and OpenedNotes.
+    QSharedPointer<VSearchResult> search(const QVector<VFile *> &p_files);
+
+    // Search folder for CurrentFolder.
+    QSharedPointer<VSearchResult> search(VDirectory *p_directory);
+
+    // Search folder for CurrentNotebook and AllNotebooks.
+    QSharedPointer<VSearchResult> search(const QVector<VNotebook *> &p_notebooks);
+
+    // Clear resources after a search completed.
+    void clear();
+
+    void stop();
+
+signals:
+    // Emitted when a new item added as result.
+    void resultItemAdded(const QSharedPointer<VSearchResultItem> &p_item);
+
+    // Emitted when async task finished.
+    void finished(const QSharedPointer<VSearchResult> &p_result);
+
+private:
+    bool askedToStop() const;
+
+    // @p_searchContent: whether search content in first phase.
+    void searchFirstPhase(VFile *p_file,
+                          const QSharedPointer<VSearchResult> &p_result,
+                          bool p_searchContent = false);
+
+    void searchFirstPhase(VDirectory *p_directory,
+                          const QSharedPointer<VSearchResult> &p_result);
+
+    void searchFirstPhase(VNotebook *p_notebook,
+                          const QSharedPointer<VSearchResult> &p_result);
+
+    bool testTarget(VSearchConfig::Target p_target) const;
+
+    bool testObject(VSearchConfig::Object p_object) const;
+
+    bool testOption(VSearchConfig::Option p_option) const;
+
+    bool matchOneLine(const QString &p_text, const QRegExp &p_reg) const;
+
+    VSearchResultItem *searchForOutline(const VFile *p_file) const;
+
+    VSearchResultItem *searchForContent(const VFile *p_file) const;
+
+    void searchSecondPhase(const QSharedPointer<VSearchResult> &p_result);
+
+    bool m_askedToStop;
+
+    QSharedPointer<VSearchConfig> m_config;
+
+    ISearchEngine *m_engine;
+
+    // Search reg used for name, outline, tag.
+    QRegExp m_searchReg;
+
+    // Search reg used for content.
+    // We use raw string to speed up if it is empty.
+    QRegExp m_contentSearchReg;
+
+    // Wildcard reg to for file name pattern.
+    QRegExp m_patternReg;
+};
+
+inline bool VSearch::askedToStop() const
+{
+    QCoreApplication::processEvents();
+    return m_askedToStop;
+}
+
+inline void VSearch::setConfig(QSharedPointer<VSearchConfig> p_config)
+{
+    m_config = p_config;
+
+    // Compile reg.
+    const QString &keyword = m_config->m_keyword;
+    m_contentSearchReg = QRegExp();
+    if (keyword.isEmpty()) {
+        m_searchReg = QRegExp();
+        return;
+    }
+
+    Qt::CaseSensitivity cs = testOption(VSearchConfig::CaseSensitive)
+                             ? Qt::CaseSensitive : Qt::CaseInsensitive;
+    if (testOption(VSearchConfig::RegularExpression)) {
+        m_searchReg = QRegExp(keyword, cs);
+        m_contentSearchReg = QRegExp(keyword, cs);
+    } else {
+        if (testOption(VSearchConfig::Fuzzy)) {
+            QString wildcardText(keyword.size() * 2 + 1, '*');
+            for (int i = 0, j = 1; i < keyword.size(); ++i, j += 2) {
+                wildcardText[j] = keyword[i];
+            }
+
+            m_searchReg = QRegExp(wildcardText, cs, QRegExp::Wildcard);
+        } else {
+            QString pattern = QRegExp::escape(keyword);
+            if (testOption(VSearchConfig::WholeWordOnly)) {
+                pattern = "\\b" + pattern + "\\b";
+
+                // We only use m_contentSearchReg when WholeWordOnly is checked.
+                m_contentSearchReg = QRegExp(pattern, cs);
+            }
+
+            m_searchReg = QRegExp(pattern, cs);
+        }
+    }
+
+    if (m_config->m_pattern.isEmpty()) {
+        m_patternReg = QRegExp();
+    } else {
+        m_patternReg = QRegExp(m_config->m_pattern, Qt::CaseInsensitive, QRegExp::Wildcard);
+    }
+}
+
+inline bool VSearch::testTarget(VSearchConfig::Target p_target) const
+{
+    return p_target & m_config->m_target;
+}
+
+inline bool VSearch::testObject(VSearchConfig::Object p_object) const
+{
+    return p_object & m_config->m_object;
+}
+
+inline bool VSearch::testOption(VSearchConfig::Option p_option) const
+{
+    return p_option & m_config->m_option;
+}
+
+inline bool VSearch::matchOneLine(const QString &p_text, const QRegExp &p_reg) const
+{
+    return p_reg.indexIn(p_text) != -1;
+}
+#endif // VSEARCH_H

+ 273 - 0
src/vsearchconfig.h

@@ -0,0 +1,273 @@
+#ifndef VSEARCHCONFIG_H
+#define VSEARCHCONFIG_H
+
+#include <QString>
+#include <QStringList>
+#include <QSharedPointer>
+
+
+struct VSearchConfig
+{
+    enum Scope
+    {
+        NoneScope = 0,
+        CurrentNote,
+        OpenedNotes,
+        CurrentFolder,
+        CurrentNotebook,
+        AllNotebooks
+    };
+
+    enum Object
+    {
+        NoneObject = 0,
+        Name = 0x1UL,
+        Content = 0x2UL,
+        Outline = 0x4UL,
+        Tag = 0x8UL
+    };
+
+    enum Target
+    {
+        NoneTarget = 0,
+        Note = 0x1UL,
+        Folder = 0x2UL,
+        Notebook = 0x4UL
+    };
+
+    enum Engine
+    {
+        Internal = 0
+    };
+
+    enum Option
+    {
+        NoneOption = 0,
+        CaseSensitive = 0x1UL,
+        WholeWordOnly = 0x2UL,
+        Fuzzy = 0x4UL,
+        RegularExpression = 0x8UL
+    };
+
+    VSearchConfig()
+        : VSearchConfig(Scope::NoneScope,
+                        Object::NoneObject,
+                        Target::NoneTarget,
+                        Engine::Internal,
+                        Option::NoneOption,
+                        "",
+                        "")
+    {
+    }
+
+
+    VSearchConfig(int p_scope,
+                  int p_object,
+                  int p_target,
+                  int p_engine,
+                  int p_option,
+                  const QString &p_keyword,
+                  const QString &p_pattern)
+        : m_scope(p_scope),
+          m_object(p_object),
+          m_target(p_target),
+          m_engine(p_engine),
+          m_option(p_option),
+          m_keyword(p_keyword),
+          m_pattern(p_pattern)
+    {
+    }
+
+    QStringList toConfig() const
+    {
+        QStringList str;
+        str << QString::number(m_scope);
+        str << QString::number(m_object);
+        str << QString::number(m_target);
+        str << QString::number(m_engine);
+        str << QString::number(m_option);
+        str << m_pattern;
+
+        return str;
+    }
+
+    static VSearchConfig fromConfig(const QStringList &p_str)
+    {
+        VSearchConfig config;
+        if (p_str.size() != 6) {
+            return config;
+        }
+
+        config.m_scope = p_str[0].toInt();
+        config.m_object = p_str[1].toInt();
+        config.m_target = p_str[2].toInt();
+        config.m_engine = p_str[3].toInt();
+        config.m_option = p_str[4].toInt();
+        config.m_pattern = p_str[5];
+
+        return config;
+    }
+
+    int m_scope;
+    int m_object;
+    int m_target;
+    int m_engine;
+    int m_option;
+
+    QString m_keyword;
+
+    // Wildcard pattern to filter file.
+    QString m_pattern;
+};
+
+
+struct VSearchResultSubItem
+{
+    VSearchResultSubItem()
+        : m_lineNumber(-1)
+    {
+    }
+
+    VSearchResultSubItem(int p_lineNumber,
+                         const QString &p_text)
+        : m_lineNumber(p_lineNumber),
+          m_text(p_text)
+    {
+    }
+
+    int m_lineNumber;
+
+    QString m_text;
+};
+
+
+struct VSearchResultItem
+{
+    enum ItemType
+    {
+        None = 0,
+        Note,
+        Folder,
+        Notebook
+    };
+
+
+    enum MatchType
+    {
+        LineNumber = 0,
+        OutlineIndex
+    };
+
+
+    VSearchResultItem()
+        : m_type(ItemType::None),
+          m_matchType(MatchType::LineNumber)
+    {
+    }
+
+    VSearchResultItem(VSearchResultItem::ItemType p_type,
+                      VSearchResultItem::MatchType p_matchType,
+                      const QString &p_text,
+                      const QString &p_path)
+        : m_type(p_type),
+          m_matchType(p_matchType),
+          m_text(p_text),
+          m_path(p_path)
+    {
+    }
+
+    bool isEmpty() const
+    {
+        return m_type == ItemType::None;
+    }
+
+    QString toString() const
+    {
+        return QString("item text: [%1] path: [%2] subitems: %3")
+                      .arg(m_text)
+                      .arg(m_path)
+                      .arg(m_matches.size());
+    }
+
+
+    ItemType m_type;
+
+    MatchType m_matchType;
+
+    // Text to displayed. If empty, use @m_path instead.
+    QString m_text;
+
+    // Path of the target.
+    QString m_path;
+
+    // Matched places within this item.
+    QList<VSearchResultSubItem> m_matches;
+};
+
+
+class VSearch;
+
+
+enum class VSearchState
+{
+    Idle = 0,
+    Busy,
+    Success,
+    Fail,
+    Cancelled
+};
+
+
+struct VSearchResult
+{
+    friend class VSearch;
+
+    VSearchResult(VSearch *p_search)
+        : m_state(VSearchState::Idle),
+          m_search(p_search)
+    {
+    }
+
+    bool hasError() const
+    {
+        return !m_errMsg.isEmpty();
+    }
+
+    void logError(const QString &p_err)
+    {
+        if (m_errMsg.isEmpty()) {
+            m_errMsg = p_err;
+        } else {
+            m_errMsg = "\n" + p_err;
+        }
+    }
+
+    void addSecondPhaseItem(const QString &p_item)
+    {
+        m_secondPhaseItems.append(p_item);
+    }
+
+    QString toString() const
+    {
+        QString str = QString("search result: state %1 err %2")
+                             .arg((int)m_state)
+                             .arg(!m_errMsg.isEmpty());
+        return str;
+    }
+
+    bool hasSecondPhaseItems() const
+    {
+        return !m_secondPhaseItems.isEmpty();
+    }
+
+    VSearchState m_state;
+
+    QString m_errMsg;
+
+    QStringList m_secondPhaseItems;
+
+private:
+    VSearch *m_search;
+};
+
+#endif // VSEARCHCONFIG_H

+ 255 - 0
src/vsearchengine.cpp

@@ -0,0 +1,255 @@
+#include "vsearchengine.h"
+
+#include <QDebug>
+#include <QFile>
+#include <QMimeDatabase>
+
+#include "utils/vutils.h"
+
+VSearchEngineWorker::VSearchEngineWorker(QObject *p_parent)
+    : QThread(p_parent),
+      m_stop(0),
+      m_state(VSearchState::Idle)
+{
+}
+
+void VSearchEngineWorker::setData(const QStringList &p_files,
+                                  const QRegExp &p_reg,
+                                  const QString &p_keyword,
+                                  Qt::CaseSensitivity p_cs)
+{
+    m_files = p_files;
+    m_reg = p_reg;
+    m_keyword = p_keyword;
+    m_caseSensitivity = p_cs;
+}
+
+void VSearchEngineWorker::stop()
+{
+    m_stop.store(1);
+}
+
+void VSearchEngineWorker::run()
+{
+    qDebug() << "worker" << QThread::currentThreadId() << m_files.size();
+
+    QMimeDatabase mimeDatabase;
+    m_state = VSearchState::Busy;
+
+    for (auto const & fileName : m_files) {
+        if (m_stop.load() == 1) {
+            m_state = VSearchState::Cancelled;
+            qDebug() << "worker" << QThread::currentThreadId() << "is asked to stop";
+            break;
+        }
+
+        const QMimeType mimeType = mimeDatabase.mimeTypeForFile(fileName);
+        if (mimeType.isValid() && !mimeType.inherits(QStringLiteral("text/plain"))) {
+            appendError(tr("Skip binary file %1.").arg(fileName));
+            continue;
+        }
+
+        VSearchResultItem *item = searchFile(fileName);
+        if (item) {
+            emit resultItemReady(item);
+        }
+    }
+
+    if (m_state == VSearchState::Busy) {
+        m_state = VSearchState::Success;
+    }
+}
+
+VSearchResultItem *VSearchEngineWorker::searchFile(const QString &p_fileName)
+{
+    QFile file(p_fileName);
+    if (!file.open(QIODevice::ReadOnly)) {
+        return NULL;
+    }
+
+    int lineNum = 1;
+    VSearchResultItem *item = NULL;
+    QString line;
+    QTextStream in(&file);
+    while (!in.atEnd()) {
+        if (m_stop.load() == 1) {
+            m_state = VSearchState::Cancelled;
+            qDebug() << "worker" << QThread::currentThreadId() << "is asked to stop";
+            break;
+        }
+
+        bool matched = false;
+        line = in.readLine();
+        if (m_reg.isEmpty()) {
+            if (line.contains(m_keyword, m_caseSensitivity)) {
+                matched = true;
+            }
+        } else if (m_reg.indexIn(line) != -1) {
+            matched = true;
+        }
+
+        if (matched) {
+            if (!item) {
+                item = new VSearchResultItem(VSearchResultItem::Note,
+                                             VSearchResultItem::LineNumber,
+                                             VUtils::fileNameFromPath(p_fileName),
+                                             p_fileName);
+            }
+
+            VSearchResultSubItem sitem(lineNum, line);
+            item->m_matches.append(sitem);
+        }
+
+        ++lineNum;
+    }
+
+    return item;
+}
+
+
+VSearchEngine::VSearchEngine(QObject *p_parent)
+    : ISearchEngine(p_parent),
+      m_finishedWorkers(0)
+{
+}
+
+void VSearchEngine::search(const QSharedPointer<VSearchConfig> &p_config,
+                           const QSharedPointer<VSearchResult> &p_result)
+{
+    int numThread = QThread::idealThreadCount();
+    if (numThread < 1) {
+        numThread = 1;
+    }
+
+    const QStringList items = p_result->m_secondPhaseItems;
+    Q_ASSERT(!items.isEmpty());
+    if (items.size() < numThread) {
+        numThread = items.size();
+    }
+
+    m_result = p_result;
+
+    QRegExp reg = compileRegExpFromConfig(p_config);
+    Qt::CaseSensitivity cs = (p_config->m_option & VSearchConfig::CaseSensitive)
+                             ? Qt::CaseSensitive : Qt::CaseInsensitive;
+
+    clearAllWorkers();
+    m_workers.reserve(numThread);
+    m_finishedWorkers = 0;
+    int totalSize = m_result->m_secondPhaseItems.size();
+    int step = totalSize / numThread;
+    int remain = totalSize % numThread;
+
+    for (int i = 0; i < numThread; ++i) {
+        int start = i * step;
+        if (start >= totalSize) {
+            break;
+        }
+
+        int len = step;
+        if (remain) {
+            ++len;
+            --remain;
+        }
+
+        if (start + len > totalSize) {
+            len = totalSize - start;
+        }
+
+        VSearchEngineWorker *th = new VSearchEngineWorker(this);
+        th->setData(m_result->m_secondPhaseItems.mid(start, len),
+                    reg,
+                    p_config->m_keyword,
+                    cs);
+        connect(th, &VSearchEngineWorker::finished,
+                this, &VSearchEngine::handleWorkerFinished);
+        connect(th, &VSearchEngineWorker::resultItemReady,
+                this, [this](VSearchResultItem *p_item) {
+                    emit resultItemAdded(QSharedPointer<VSearchResultItem>(p_item));
+                });
+
+        m_workers.append(th);
+        th->start();
+    }
+
+    qDebug() << "schedule tasks to threads" << m_workers.size() << totalSize << step;
+}
+
+QRegExp VSearchEngine::compileRegExpFromConfig(const QSharedPointer<VSearchConfig> &p_config) const
+{
+    const QString &keyword = p_config->m_keyword;
+    Qt::CaseSensitivity cs = (p_config->m_option & VSearchConfig::CaseSensitive)
+                             ? Qt::CaseSensitive : Qt::CaseInsensitive;
+    if (p_config->m_option & VSearchConfig::RegularExpression) {
+        return QRegExp(keyword, cs);
+    } else if (p_config->m_option & VSearchConfig::WholeWordOnly) {
+        QString pattern = QRegExp::escape(keyword);
+        pattern = "\\b" + pattern + "\\b";
+        return QRegExp(pattern, cs);
+    } else {
+        return QRegExp();
+    }
+}
+
+void VSearchEngine::stop()
+{
+    qDebug() << "VSearchEngine asked to stop";
+    for (auto const & th : m_workers) {
+        th->stop();
+    }
+}
+
+void VSearchEngine::handleWorkerFinished()
+{
+    ++m_finishedWorkers;
+
+    qDebug() << m_finishedWorkers << "workers finished";
+    if (m_finishedWorkers == m_workers.size()) {
+        VSearchState state = VSearchState::Success;
+
+        for (auto const & th : m_workers) {
+            if (th->m_state == VSearchState::Fail) {
+                if (state != VSearchState::Cancelled) {
+                    state = VSearchState::Fail;
+                }
+            } else if (th->m_state == VSearchState::Cancelled) {
+                state = VSearchState::Cancelled;
+            }
+
+            if (!th->m_error.isEmpty()) {
+                m_result->logError(th->m_error);
+            }
+
+            Q_ASSERT(th->isFinished());
+            th->deleteLater();
+        }
+
+        m_workers.clear();
+        m_finishedWorkers = 0;
+
+        m_result->m_state = state;
+        qDebug() << "SearchEngine finished" << (int)state;
+        emit finished(m_result);
+    }
+}
+
+void VSearchEngine::clear()
+{
+    clearAllWorkers();
+
+    m_finishedWorkers = 0;
+
+    m_result.clear();
+}
+
+void VSearchEngine::clearAllWorkers()
+{
+    for (auto const & th : m_workers) {
+        th->quit();
+        th->wait();
+
+        delete th;
+    }
+
+    m_workers.clear();
+}

+ 89 - 0
src/vsearchengine.h

@@ -0,0 +1,89 @@
+#ifndef VSEARCHENGINE_H
+#define VSEARCHENGINE_H
+
+#include <QThread>
+#include <QRegExp>
+#include <QAtomicInt>
+#include "isearchengine.h"
+
+class VSearchEngineWorker : public QThread
+{
+    Q_OBJECT
+
+    friend class VSearchEngine;
+
+public:
+    explicit VSearchEngineWorker(QObject *p_parent = nullptr);
+
+    void setData(const QStringList &p_files,
+                 const QRegExp &p_reg,
+                 const QString &p_keyword,
+                 Qt::CaseSensitivity p_cs);
+
+public slots:
+    void stop();
+
+signals:
+    void resultItemReady(VSearchResultItem *p_item);
+
+protected:
+    void run() Q_DECL_OVERRIDE;
+
+private:
+    void appendError(const QString &p_err);
+
+    VSearchResultItem *searchFile(const QString &p_fileName);
+
+    QAtomicInt m_stop;
+
+    QStringList m_files;
+
+    QRegExp m_reg;
+
+    QString m_keyword;
+
+    Qt::CaseSensitivity m_caseSensitivity;
+
+    VSearchState m_state;
+
+    QString m_error;
+};
+
+inline void VSearchEngineWorker::appendError(const QString &p_err)
+{
+    if (m_error.isEmpty()) {
+        m_error = p_err;
+    } else {
+        m_error = "\n" + p_err;
+    }
+}
+
+
+class VSearchEngine : public ISearchEngine
+{
+    Q_OBJECT
+public:
+    explicit VSearchEngine(QObject *p_parent = nullptr);
+
+    void search(const QSharedPointer<VSearchConfig> &p_config,
+                const QSharedPointer<VSearchResult> &p_result) Q_DECL_OVERRIDE;
+
+    void stop() Q_DECL_OVERRIDE;
+
+    void clear() Q_DECL_OVERRIDE;
+
+private slots:
+    void handleWorkerFinished();
+
+private:
+    // Returns an empty object if raw string is preferred.
+    QRegExp compileRegExpFromConfig(const QSharedPointer<VSearchConfig> &p_config) const;
+
+    void clearAllWorkers();
+
+    int m_finishedWorkers;
+
+    QVector<VSearchEngineWorker *> m_workers;
+};
+
+#endif // VSEARCHENGINE_H

+ 548 - 0
src/vsearcher.cpp

@@ -0,0 +1,548 @@
+#include "vsearcher.h"
+
+#include <QtWidgets>
+#include <QCoreApplication>
+
+#include "vlineedit.h"
+#include "utils/vutils.h"
+#include "utils/viconutils.h"
+#include "vsearch.h"
+#include "vsearchresulttree.h"
+#include "vmainwindow.h"
+#include "veditarea.h"
+#include "vdirectorytree.h"
+#include "vdirectory.h"
+#include "vnotebookselector.h"
+#include "vnotebook.h"
+#include "vnote.h"
+#include "vconfigmanager.h"
+
+extern VMainWindow *g_mainWin;
+
+extern VNote *g_vnote;
+
+extern VConfigManager *g_config;
+
+VSearcher::VSearcher(QWidget *p_parent)
+    : QWidget(p_parent),
+      m_inSearch(false),
+      m_askedToStop(false),
+      m_search(this)
+{
+    setupUI();
+
+    initUIFields();
+
+    handleInputChanged();
+
+    connect(&m_search, &VSearch::resultItemAdded,
+            m_results, &VSearchResultTree::addResultItem);
+    connect(&m_search, &VSearch::finished,
+            this, &VSearcher::handleSearchFinished);
+}
+
+void VSearcher::setupUI()
+{
+    // Search button.
+    m_searchBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/search.svg"), "", this);
+    m_searchBtn->setToolTip(tr("Search"));
+    m_searchBtn->setProperty("FlatBtn", true);
+    connect(m_searchBtn, &QPushButton::clicked,
+            this, &VSearcher::startSearch);
+
+    // Clear button.
+    m_clearBtn = new QPushButton(VIconUtils::buttonDangerIcon(":/resources/icons/clear_search.svg"), "", this);
+    m_clearBtn->setToolTip(tr("Clear Results"));
+    m_clearBtn->setProperty("FlatBtn", true);
+    connect(m_clearBtn, &QPushButton::clicked,
+            this, [this]() {
+                m_results->clearResults();
+            });
+
+    // Advanced button.
+    m_advBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/search_advanced.svg"),
+                               "",
+                               this);
+    m_advBtn->setToolTip(tr("Advanced Settings"));
+    m_advBtn->setProperty("FlatBtn", true);
+    m_advBtn->setCheckable(true);
+    connect(m_advBtn, &QPushButton::toggled,
+            this, [this](bool p_checked) {
+                m_advWidget->setVisible(p_checked);
+            });
+
+    // Console button.
+    m_consoleBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/search_console.svg"),
+                                   "",
+                                   this);
+    m_consoleBtn->setToolTip(tr("Console"));
+    m_consoleBtn->setProperty("FlatBtn", true);
+    m_consoleBtn->setCheckable(true);
+    connect(m_consoleBtn, &QPushButton::toggled,
+            this, [this](bool p_checked) {
+                m_consoleEdit->setVisible(p_checked);
+            });
+
+    m_numLabel = new QLabel(this);
+
+    QHBoxLayout *btnLayout = new QHBoxLayout();
+    btnLayout->addWidget(m_searchBtn);
+    btnLayout->addWidget(m_clearBtn);
+    btnLayout->addWidget(m_advBtn);
+    btnLayout->addWidget(m_consoleBtn);
+    btnLayout->addStretch();
+    btnLayout->addWidget(m_numLabel);
+    btnLayout->setContentsMargins(0, 0, 3, 0);
+
+    // Keyword.
+    m_keywordCB = VUtils::getComboBox(this);
+    m_keywordCB->setEditable(true);
+    m_keywordCB->setLineEdit(new VLineEdit(this));
+    m_keywordCB->setToolTip(tr("Keywords to search for"));
+    connect(m_keywordCB, &QComboBox::currentTextChanged,
+            this, &VSearcher::handleInputChanged);
+    connect(m_keywordCB->lineEdit(), &QLineEdit::returnPressed,
+            this, &VSearcher::animateSearchClick);
+    m_keywordCB->completer()->setCaseSensitivity(Qt::CaseSensitive);
+
+    // Scope.
+    m_searchScopeCB = VUtils::getComboBox(this);
+    m_searchScopeCB->setToolTip(tr("Scope to search"));
+    connect(m_searchScopeCB, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+            this, &VSearcher::handleInputChanged);
+
+    // Object.
+    m_searchObjectCB = VUtils::getComboBox(this);
+    m_searchObjectCB->setToolTip(tr("Object to search"));
+    connect(m_searchObjectCB, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+            this, &VSearcher::handleInputChanged);
+
+    // Target.
+    m_searchTargetCB = VUtils::getComboBox(this);
+    m_searchTargetCB->setToolTip(tr("Target to search"));
+    connect(m_searchTargetCB, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+            this, &VSearcher::handleInputChanged);
+
+    // Pattern.
+    m_filePatternCB = VUtils::getComboBox(this);
+    m_filePatternCB->setEditable(true);
+    m_filePatternCB->setLineEdit(new VLineEdit(this));
+    m_filePatternCB->setToolTip(tr("Wildcard pattern to filter the files to be searched"));
+    m_filePatternCB->completer()->setCaseSensitivity(Qt::CaseSensitive);
+
+    // Engine.
+    m_searchEngineCB = VUtils::getComboBox(this);
+    m_searchEngineCB->setToolTip(tr("Engine to execute the search"));
+
+    // Case sensitive.
+    m_caseSensitiveCB = new QCheckBox(tr("&Case sensitive"), this);
+
+    // Whole word only.
+    m_wholeWordOnlyCB = new QCheckBox(tr("&Whole word only"), this);
+
+    // Fuzzy search.
+    m_fuzzyCB = new QCheckBox(tr("&Fuzzy search"), this);
+    m_fuzzyCB->setToolTip(tr("Not available for content search"));
+    connect(m_fuzzyCB, &QCheckBox::stateChanged,
+            this, [this](int p_state) {
+                bool checked = p_state == Qt::Checked;
+                m_wholeWordOnlyCB->setEnabled(!checked);
+            });
+
+    // Regular expression.
+    m_regularExpressionCB = new QCheckBox(tr("Re&gular expression"), this);
+    connect(m_regularExpressionCB, &QCheckBox::stateChanged,
+            this, [this](int p_state) {
+                bool checked = p_state == Qt::Checked;
+                m_wholeWordOnlyCB->setEnabled(!checked);
+                m_fuzzyCB->setEnabled(!checked);
+            });
+
+    QFormLayout *advLayout = new QFormLayout();
+    advLayout->addRow(tr("File pattern:"), m_filePatternCB);
+    advLayout->addRow(tr("Engine:"), m_searchEngineCB);
+    advLayout->addRow(m_caseSensitiveCB);
+    advLayout->addRow(m_wholeWordOnlyCB);
+    advLayout->addRow(m_fuzzyCB);
+    advLayout->addRow(m_regularExpressionCB);
+    advLayout->setContentsMargins(0, 0, 0, 0);
+
+    m_advWidget = new QWidget(this);
+    m_advWidget->setLayout(advLayout);
+    m_advWidget->hide();
+
+    // Progress bar.
+    m_proBar = new QProgressBar(this);
+    m_proBar->setRange(0, 0);
+
+    // Cancel button.
+    m_cancelBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/close.svg"),
+                                  "",
+                                  this);
+    m_cancelBtn->setToolTip(tr("Cancel"));
+    m_cancelBtn->setProperty("FlatBtn", true);
+    connect(m_cancelBtn, &QPushButton::clicked,
+            this, [this]() {
+                if (m_inSearch) {
+                    appendLogLine(tr("Cancelling the export..."));
+                    m_askedToStop = true;
+                    m_search.stop();
+                }
+            });
+
+    QHBoxLayout *proLayout = new QHBoxLayout();
+    proLayout->addWidget(m_proBar);
+    proLayout->addWidget(m_cancelBtn);
+    proLayout->setContentsMargins(0, 0, 0, 0);
+
+    // Console.
+    m_consoleEdit = new QPlainTextEdit(this);
+    m_consoleEdit->setReadOnly(true);
+    m_consoleEdit->setLineWrapMode(QPlainTextEdit::WidgetWidth);
+    m_consoleEdit->setProperty("LineEdit", true);
+    m_consoleEdit->setPlaceholderText(tr("Output logs will be shown here"));
+    m_consoleEdit->setMaximumHeight(m_searchScopeCB->height() * 2);
+    m_consoleEdit->hide();
+
+    // List.
+    m_results = new VSearchResultTree(this);
+    m_results->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+    connect(m_results, &VSearchResultTree::countChanged,
+            this, [this](int p_count) {
+                m_clearBtn->setEnabled(p_count > 0);
+                updateNumLabel(p_count);
+            });
+
+    QFormLayout *formLayout = new QFormLayout();
+    formLayout->addRow(tr("Keywords:"), m_keywordCB);
+    formLayout->addRow(tr("Scope:"), m_searchScopeCB);
+    formLayout->addRow(tr("Object:"), m_searchObjectCB);
+    formLayout->addRow(tr("Target:"), m_searchTargetCB);
+    formLayout->setContentsMargins(0, 0, 0, 0);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addLayout(btnLayout);
+    mainLayout->addLayout(formLayout);
+    mainLayout->addWidget(m_advWidget);
+    mainLayout->addLayout(proLayout);
+    mainLayout->addWidget(m_consoleEdit);
+    mainLayout->addWidget(m_results);
+
+    setLayout(mainLayout);
+}
+
+void VSearcher::initUIFields()
+{
+    VSearchConfig config = VSearchConfig::fromConfig(g_config->getSearchOptions());
+
+    // Scope.
+    m_searchScopeCB->addItem(tr("Current Note"), VSearchConfig::CurrentNote);
+    m_searchScopeCB->addItem(tr("Opened Notes"), VSearchConfig::OpenedNotes);
+    m_searchScopeCB->addItem(tr("Current Folder"), VSearchConfig::CurrentFolder);
+    m_searchScopeCB->addItem(tr("Current Notebook"), VSearchConfig::CurrentNotebook);
+    m_searchScopeCB->addItem(tr("All Notebooks"), VSearchConfig::AllNotebooks);
+    m_searchScopeCB->setCurrentIndex(m_searchScopeCB->findData(config.m_scope));
+
+    // Object.
+    m_searchObjectCB->addItem(tr("Name"), VSearchConfig::Name);
+    m_searchObjectCB->addItem(tr("Content"), VSearchConfig::Content);
+    m_searchObjectCB->addItem(tr("Outline"), VSearchConfig::Outline);
+    m_searchObjectCB->setCurrentIndex(m_searchObjectCB->findData(config.m_object));
+
+    // Target.
+    m_searchTargetCB->addItem(tr("Note"), VSearchConfig::Note);
+    m_searchTargetCB->addItem(tr("Folder"), VSearchConfig::Folder);
+    m_searchTargetCB->addItem(tr("Notebook"), VSearchConfig::Notebook);
+    m_searchTargetCB->addItem(tr("All"),
+                              VSearchConfig::Note
+                              | VSearchConfig:: Folder
+                              | VSearchConfig::Notebook);
+    m_searchTargetCB->setCurrentIndex(m_searchTargetCB->findData(config.m_target));
+
+    // Engine.
+    m_searchEngineCB->addItem(tr("Internal"), VSearchConfig::Internal);
+    m_searchEngineCB->setCurrentIndex(m_searchEngineCB->findData(config.m_engine));
+
+    // Pattern.
+    m_filePatternCB->setCurrentText(config.m_pattern);
+
+    m_caseSensitiveCB->setChecked(config.m_option & VSearchConfig::CaseSensitive);
+    m_wholeWordOnlyCB->setChecked(config.m_option & VSearchConfig::WholeWordOnly);
+    m_fuzzyCB->setChecked(config.m_option & VSearchConfig::Fuzzy);
+    m_regularExpressionCB->setChecked(config.m_option & VSearchConfig::RegularExpression);
+
+    setProgressVisible(false);
+
+    m_clearBtn->setEnabled(false);
+}
+
+void VSearcher::updateItemToComboBox(QComboBox *p_comboBox)
+{
+    QString text = p_comboBox->currentText();
+    if (!text.isEmpty() && p_comboBox->findText(text) == -1) {
+        p_comboBox->addItem(text);
+    }
+}
+
+void VSearcher::setProgressVisible(bool p_visible)
+{
+    m_proBar->setVisible(p_visible);
+    m_cancelBtn->setVisible(p_visible);
+}
+
+void VSearcher::appendLogLine(const QString &p_text)
+{
+    m_consoleEdit->appendPlainText(">>> " + p_text);
+    m_consoleEdit->ensureCursorVisible();
+    QCoreApplication::sendPostedEvents();
+}
+
+void VSearcher::showMessage(const QString &p_text) const
+{
+    g_mainWin->showStatusMessage(p_text);
+    QCoreApplication::sendPostedEvents();
+}
+
+void VSearcher::handleInputChanged()
+{
+    bool readyToSearch = true;
+
+    // Keyword.
+    QString keyword = m_keywordCB->currentText();
+    readyToSearch = !keyword.isEmpty();
+
+    if (readyToSearch) {
+        // Other targets are only available for Name.
+        int obj = m_searchObjectCB->currentData().toInt();
+        if (obj != VSearchConfig::Name) {
+            int target = m_searchTargetCB->currentData().toInt();
+            if (!(target & VSearchConfig::Note)) {
+                readyToSearch = false;
+            }
+        }
+
+        if (readyToSearch && obj == VSearchConfig::Outline) {
+            // Outline is available only for CurrentNote and OpenedNotes.
+            int scope = m_searchScopeCB->currentData().toInt();
+            if (scope != VSearchConfig::CurrentNote
+                && scope != VSearchConfig::OpenedNotes) {
+                readyToSearch = false;
+            }
+        }
+    }
+
+    m_searchBtn->setEnabled(readyToSearch);
+}
+
+void VSearcher::startSearch()
+{
+    if (m_inSearch) {
+        return;
+    }
+
+    m_searchBtn->setEnabled(false);
+    setProgressVisible(true);
+    m_results->clearResults();
+    m_askedToStop = false;
+    m_inSearch = true;
+    m_consoleEdit->clear();
+    appendLogLine(tr("Search started."));
+
+    updateItemToComboBox(m_keywordCB);
+    updateItemToComboBox(m_filePatternCB);
+
+    QSharedPointer<VSearchConfig> config(new VSearchConfig(m_searchScopeCB->currentData().toInt(),
+                                                           m_searchObjectCB->currentData().toInt(),
+                                                           m_searchTargetCB->currentData().toInt(),
+                                                           m_searchEngineCB->currentData().toInt(),
+                                                           getSearchOption(),
+                                                           m_keywordCB->currentText(),
+                                                           m_filePatternCB->currentText()));
+    m_search.setConfig(config);
+
+    g_config->setSearchOptions(config->toConfig());
+
+    QSharedPointer<VSearchResult> result;
+    switch (config->m_scope) {
+    case VSearchConfig::CurrentNote:
+    {
+        QVector<VFile *> files;
+        files.append(g_mainWin->getCurrentFile());
+        if (files[0]) {
+            QString msg(tr("Search current note %1.").arg(files[0]->getName()));
+            appendLogLine(msg);
+            showMessage(msg);
+        }
+
+        result = m_search.search(files);
+        break;
+    }
+
+    case VSearchConfig::OpenedNotes:
+    {
+        QVector<VEditTabInfo> tabs = g_mainWin->getEditArea()->getAllTabsInfo();
+        QVector<VFile *> files;
+        files.reserve(tabs.size());
+        for (auto const & ta : tabs) {
+            files.append(ta.m_editTab->getFile());
+        }
+
+        result = m_search.search(files);
+        break;
+    }
+
+    case VSearchConfig::CurrentFolder:
+    {
+        VDirectory *dir = g_mainWin->getDirectoryTree()->currentDirectory();
+        if (dir) {
+            QString msg(tr("Search current folder %1.").arg(dir->getName()));
+            appendLogLine(msg);
+            showMessage(msg);
+        }
+
+        result = m_search.search(dir);
+        break;
+    }
+
+    case VSearchConfig::CurrentNotebook:
+    {
+        QVector<VNotebook *> notebooks;
+        notebooks.append(g_mainWin->getNotebookSelector()->currentNotebook());
+        if (notebooks[0]) {
+            QString msg(tr("Search current notebook %1.").arg(notebooks[0]->getName()));
+            appendLogLine(msg);
+            showMessage(msg);
+        }
+
+        result = m_search.search(notebooks);
+        break;
+    }
+
+    case VSearchConfig::AllNotebooks:
+    {
+        const QVector<VNotebook *> &notebooks = g_vnote->getNotebooks();
+        result = m_search.search(notebooks);
+        break;
+    }
+
+    default:
+        break;
+    }
+
+    handleSearchFinished(result);
+}
+
+void VSearcher::handleSearchFinished(const QSharedPointer<VSearchResult> &p_result)
+{
+    Q_ASSERT(m_inSearch);
+    Q_ASSERT(p_result->m_state != VSearchState::Idle);
+
+    qDebug() << "handleSearchFinished" << (int)p_result->m_state;
+
+    QString msg;
+    switch (p_result->m_state) {
+    case VSearchState::Busy:
+        msg = tr("Search is on going.");
+        appendLogLine(msg);
+        return;
+
+    case VSearchState::Success:
+        msg = tr("Search succeeded.");
+        appendLogLine(msg);
+        showMessage(msg);
+        break;
+
+    case VSearchState::Fail:
+        msg = tr("Search failed.");
+        appendLogLine(msg);
+        showMessage(msg);
+        VUtils::showMessage(QMessageBox::Warning,
+                            tr("Warning"),
+                            tr("Search failed."),
+                            p_result->m_errMsg,
+                            QMessageBox::Ok,
+                            QMessageBox::Ok,
+                            this);
+        break;
+
+    case VSearchState::Cancelled:
+        Q_ASSERT(m_askedToStop);
+        msg = tr("User cancelled the search. Aborted!");
+        appendLogLine(msg);
+        showMessage(msg);
+        m_askedToStop = false;
+        break;
+
+    default:
+        break;
+    }
+
+    if (p_result->m_state != VSearchState::Fail
+        && p_result->hasError()) {
+        VUtils::showMessage(QMessageBox::Warning,
+                            tr("Warning"),
+                            tr("Errors found during search."),
+                            p_result->m_errMsg,
+                            QMessageBox::Ok,
+                            QMessageBox::Ok,
+                            this);
+    }
+
+    m_search.clear();
+
+    m_inSearch = false;
+    m_searchBtn->setEnabled(true);
+    setProgressVisible(false);
+}
+
+void VSearcher::animateSearchClick()
+{
+    m_searchBtn->animateClick();
+}
+
+int VSearcher::getSearchOption() const
+{
+    int ret = VSearchConfig::NoneOption;
+
+    if (m_caseSensitiveCB->isChecked()) {
+        ret |= VSearchConfig::CaseSensitive;
+    }
+
+    if (m_wholeWordOnlyCB->isChecked()) {
+        ret |= VSearchConfig::WholeWordOnly;
+    }
+
+    if (m_fuzzyCB->isChecked()) {
+        ret |= VSearchConfig::Fuzzy;
+    }
+
+    if (m_regularExpressionCB->isChecked()) {
+        ret |= VSearchConfig::RegularExpression;
+    }
+
+    return ret;
+}
+
+void VSearcher::updateNumLabel(int p_count)
+{
+    m_numLabel->setText(tr("%1 Items").arg(p_count));
+}
+
+void VSearcher::focusToSearch()
+{
+    m_keywordCB->setFocus(Qt::OtherFocusReason);
+}
+
+void VSearcher::showNavigation()
+{
+    VNavigationMode::showNavigation(m_results);
+}
+
+bool VSearcher::handleKeyNavigation(int p_key, bool &p_succeed)
+{
+    static bool secondKey = false;
+    return VNavigationMode::handleKeyNavigation(m_results,
+                                                secondKey,
+                                                p_key,
+                                                p_succeed);
+}

+ 108 - 0
src/vsearcher.h

@@ -0,0 +1,108 @@
+#ifndef VSEARCHER_H
+#define VSEARCHER_H
+
+#include <QWidget>
+#include <QSharedPointer>
+
+#include "vsearch.h"
+
+#include "vnavigationmode.h"
+
+class QComboBox;
+class QCheckBox;
+class QPushButton;
+class QLabel;
+class VSearchResultTree;
+class QProgressBar;
+class QPlainTextEdit;
+
+class VSearcher : public QWidget, public VNavigationMode
+{
+    Q_OBJECT
+public:
+    explicit VSearcher(QWidget *p_parent = nullptr);
+
+    void focusToSearch();
+
+    // Implementations for VNavigationMode.
+    void showNavigation() Q_DECL_OVERRIDE;
+    bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE;
+
+private slots:
+    void handleSearchFinished(const QSharedPointer<VSearchResult> &p_result);
+
+private:
+    void startSearch();
+
+    void setupUI();
+
+    void initUIFields();
+
+    void setProgressVisible(bool p_visible);
+
+    void appendLogLine(const QString &p_text);
+
+    void handleInputChanged();
+
+    void animateSearchClick();
+
+    void updateItemToComboBox(QComboBox *p_comboBox);
+
+    // Get the OR of the search options.
+    int getSearchOption() const;
+
+    void updateNumLabel(int p_count);
+
+    void showMessage(const QString &p_text) const;
+
+    QComboBox *m_keywordCB;
+
+    // All notebooks, current notebook, and so on.
+    QComboBox *m_searchScopeCB;
+
+    // Name, content, tag.
+    QComboBox *m_searchObjectCB;
+
+    // Notebook, folder, note.
+    QComboBox *m_searchTargetCB;
+
+    QComboBox *m_filePatternCB;
+
+    QComboBox *m_searchEngineCB;
+
+    QCheckBox *m_caseSensitiveCB;
+
+    QCheckBox *m_wholeWordOnlyCB;
+
+    QCheckBox *m_fuzzyCB;
+
+    QCheckBox *m_regularExpressionCB;
+
+    QPushButton *m_searchBtn;
+
+    QPushButton *m_clearBtn;
+
+    QPushButton *m_advBtn;
+
+    QPushButton *m_consoleBtn;
+
+    QLabel *m_numLabel;
+
+    QWidget *m_advWidget;
+
+    VSearchResultTree *m_results;
+
+    QProgressBar *m_proBar;
+
+    QPushButton *m_cancelBtn;
+
+    QPlainTextEdit *m_consoleEdit;
+
+    bool m_inSearch;
+
+    bool m_askedToStop;
+
+    VSearch m_search;
+};
+
+#endif // VSEARCHER_H

+ 143 - 0
src/vsearchresulttree.cpp

@@ -0,0 +1,143 @@
+#include "vsearchresulttree.h"
+
+#include "utils/viconutils.h"
+#include "vnote.h"
+#include "vmainwindow.h"
+#include "vnotebookselector.h"
+
+extern VNote *g_vnote;
+
+extern VMainWindow *g_mainWin;
+
+VSearchResultTree::VSearchResultTree(QWidget *p_parent)
+    : VTreeWidget(p_parent)
+{
+    setColumnCount(1);
+    setHeaderHidden(true);
+    setExpandsOnDoubleClick(false);
+
+    setSimpleSearchMatchFlags(getSimpleSearchMatchFlags() & ~Qt::MatchRecursive);
+
+    m_noteIcon = VIconUtils::treeViewIcon(":/resources/icons/note_item.svg");
+    m_folderIcon = VIconUtils::treeViewIcon(":/resources/icons/dir_item.svg");
+    m_notebookIcon = VIconUtils::treeViewIcon(":/resources/icons/notebook_item.svg");
+
+    connect(this, &VTreeWidget::itemActivated,
+            this, &VSearchResultTree::handleItemActivated);
+}
+
+void VSearchResultTree::updateResults(const QList<QSharedPointer<VSearchResultItem> > &p_items)
+{
+    clearResults();
+
+    for (auto const & it : p_items) {
+        appendItem(it);
+    }
+
+    emit countChanged(topLevelItemCount());
+}
+
+void VSearchResultTree::addResultItem(const QSharedPointer<VSearchResultItem> &p_item)
+{
+    appendItem(p_item);
+
+    emit countChanged(topLevelItemCount());
+}
+
+void VSearchResultTree::clearResults()
+{
+    clearAll();
+
+    m_data.clear();
+
+    emit countChanged(topLevelItemCount());
+}
+
+void VSearchResultTree::appendItem(const QSharedPointer<VSearchResultItem> &p_item)
+{
+    m_data.append(p_item);
+
+    QTreeWidgetItem *item = new QTreeWidgetItem(this);
+    item->setData(0, Qt::UserRole, m_data.size() - 1);
+    item->setText(0, p_item->m_text.isEmpty() ? p_item->m_path : p_item->m_text);
+    item->setToolTip(0, p_item->m_path);
+
+    switch (p_item->m_type) {
+    case VSearchResultItem::Note:
+        item->setIcon(0, m_noteIcon);
+        break;
+
+    case VSearchResultItem::Folder:
+        item->setIcon(0, m_folderIcon);
+        break;
+
+    case VSearchResultItem::Notebook:
+        item->setIcon(0, m_notebookIcon);
+        break;
+
+    default:
+        break;
+    }
+
+    for (auto const & it: p_item->m_matches) {
+        QTreeWidgetItem *subItem = new QTreeWidgetItem(item);
+        QString text;
+        if (it.m_lineNumber > -1) {
+            text = QString("[%1] %2").arg(it.m_lineNumber).arg(it.m_text);
+        } else {
+            text = it.m_text;
+        }
+
+        subItem->setText(0, text);
+        subItem->setToolTip(0, it.m_text);
+    }
+}
+
+void VSearchResultTree::handleItemActivated(QTreeWidgetItem *p_item, int p_column)
+{
+    Q_UNUSED(p_column);
+    if (!p_item) {
+        return;
+    }
+
+    QTreeWidgetItem *topItem = p_item;
+    if (p_item->parent()) {
+        topItem = p_item->parent();
+    }
+
+    int idx = topItem->data(0, Qt::UserRole).toInt();
+    Q_ASSERT(idx >= 0 && idx < m_data.size());
+
+    const QSharedPointer<VSearchResultItem> &resItem = m_data[idx];
+    switch (resItem->m_type) {
+    case VSearchResultItem::Note:
+    {
+        QStringList files(resItem->m_path);
+        g_mainWin->openFiles(files);
+        break;
+    }
+
+    case VSearchResultItem::Folder:
+    {
+        VDirectory *dir = g_vnote->getInternalDirectory(resItem->m_path);
+        if (dir) {
+            g_mainWin->locateDirectory(dir);
+        }
+
+        break;
+    }
+
+    case VSearchResultItem::Notebook:
+    {
+        VNotebook *nb = g_vnote->getNotebook(resItem->m_path);
+        if (nb) {
+            g_mainWin->getNotebookSelector()->locateNotebook(nb);
+        }
+
+        break;
+    }
+
+    default:
+        break;
+    }
+}

+ 39 - 0
src/vsearchresulttree.h

@@ -0,0 +1,39 @@
+#ifndef VSEARCHRESULTTREE_H
+#define VSEARCHRESULTTREE_H
+
+#include <QIcon>
+
+#include "vtreewidget.h"
+#include "vsearch.h"
+
+
+class VSearchResultTree : public VTreeWidget
+{
+    Q_OBJECT
+public:
+    explicit VSearchResultTree(QWidget *p_parent = nullptr);
+
+    void updateResults(const QList<QSharedPointer<VSearchResultItem> > &p_items);
+
+    void clearResults();
+
+public slots:
+    void addResultItem(const QSharedPointer<VSearchResultItem> &p_item);
+
+signals:
+    void countChanged(int p_count);
+
+private slots:
+    void handleItemActivated(QTreeWidgetItem *p_item, int p_column);
+
+private:
+    void appendItem(const QSharedPointer<VSearchResultItem> &p_item);
+
+    QVector<QSharedPointer<VSearchResultItem> > m_data;
+
+    QIcon m_noteIcon;
+    QIcon m_folderIcon;
+    QIcon m_notebookIcon;
+};
+
+#endif // VSEARCHRESULTTREE_H

+ 14 - 0
src/vsimplesearchinput.h

@@ -47,6 +47,10 @@ public:
 
     void setNavigationKeyEnabled(bool p_enabled);
 
+    void setMatchFlags(Qt::MatchFlags p_flags);
+
+    Qt::MatchFlags getMatchFlags() const;
+
 signals:
     // Search mode is triggered.
     void triggered(bool p_inSearchMode);
@@ -101,4 +105,14 @@ inline void VSimpleSearchInput::setNavigationKeyEnabled(bool p_enabled)
 {
     m_navigationKeyEnabled = p_enabled;
 }
+
+inline void VSimpleSearchInput::setMatchFlags(Qt::MatchFlags p_flags)
+{
+    m_matchFlags = p_flags;
+}
+
+inline Qt::MatchFlags VSimpleSearchInput::getMatchFlags() const
+{
+    return m_matchFlags;
+}
 #endif // VSIMPLESEARCHINPUT_H

+ 14 - 1
src/vtextedit.cpp

@@ -43,6 +43,8 @@ void VTextEdit::init()
 {
     setAcceptRichText(false);
 
+    m_defaultCursorWidth = 1;
+
     m_lineNumberType = LineNumberType::None;
 
     m_blockImageEnabled = false;
@@ -60,6 +62,8 @@ void VTextEdit::init()
 
     docLayout->setVirtualCursorBlockWidth(VIRTUAL_CURSOR_BLOCK_WIDTH);
 
+    docLayout->setCursorWidth(m_defaultCursorWidth);
+
     connect(docLayout, &VTextDocumentLayout::cursorBlockWidthUpdated,
             this, [this](int p_width) {
                 if (p_width != cursorWidth()
@@ -361,7 +365,7 @@ void VTextEdit::setCursorBlockMode(CursorBlock p_mode)
         layout->setCursorBlockMode(m_cursorBlockMode);
         layout->clearLastCursorBlockWidth();
         setCursorWidth(m_cursorBlockMode != CursorBlock::None ? VIRTUAL_CURSOR_BLOCK_WIDTH
-                                                              : 1);
+                                                              : m_defaultCursorWidth);
         layout->updateBlockByNumber(textCursor().blockNumber());
     }
 }
@@ -388,3 +392,12 @@ void VTextEdit::relayout()
 {
     getLayout()->relayout();
 }
+
+void VTextEdit::setDisplayScaleFactor(qreal p_factor)
+{
+    m_defaultCursorWidth = p_factor + 0.5;
+
+    setCursorWidth(m_cursorBlockMode != CursorBlock::None ? VIRTUAL_CURSOR_BLOCK_WIDTH
+                                                          : m_defaultCursorWidth);
+    getLayout()->setCursorWidth(m_defaultCursorWidth);
+}

+ 4 - 0
src/vtextedit.h

@@ -65,6 +65,8 @@ public:
 
     void relayout();
 
+    void setDisplayScaleFactor(qreal p_factor);
+
 protected:
     void resizeEvent(QResizeEvent *p_event) Q_DECL_OVERRIDE;
 
@@ -91,6 +93,8 @@ private:
     CursorBlock m_cursorBlockMode;
 
     bool m_highlightCursorLineBlock;
+
+    int m_defaultCursorWidth;
 };
 
 inline void VTextEdit::setLineNumberType(LineNumberType p_type)

+ 14 - 0
src/vtreewidget.h

@@ -21,6 +21,10 @@ public:
     // Clear tree widget as well as other data.
     void clearAll();
 
+    void setSimpleSearchMatchFlags(Qt::MatchFlags p_flags);
+
+    Qt::MatchFlags getSimpleSearchMatchFlags() const;
+
     // Implement ISimpleSearch.
     virtual QList<void *> searchItems(const QString &p_text,
                                       Qt::MatchFlags p_flags) const Q_DECL_OVERRIDE;
@@ -65,4 +69,14 @@ private:
 
     QTimer *m_searchColdTimer;
 };
+
+inline void VTreeWidget::setSimpleSearchMatchFlags(Qt::MatchFlags p_flags)
+{
+    m_searchInput->setMatchFlags(p_flags);
+}
+
+inline Qt::MatchFlags VTreeWidget::getSimpleSearchMatchFlags() const
+{
+    return m_searchInput->getMatchFlags();
+}
 #endif // VTREEWIDGET_H