فهرست منبع

support full-text search (#1733)

Le Tan 4 سال پیش
والد
کامیت
fe827e74f0
75فایلهای تغییر یافته به همراه3100 افزوده شده و 114 حذف شده
  1. 17 9
      src/commandlineoptions.cpp
  2. 1 0
      src/core/core.pri
  3. 2 0
      src/core/coreconfig.h
  4. 3 0
      src/core/fileopenparameters.h
  5. 4 2
      src/core/global.h
  6. 72 0
      src/core/location.h
  7. 13 0
      src/core/sessionconfig.cpp
  8. 6 0
      src/core/sessionconfig.h
  9. 1 0
      src/core/vnotex.cpp
  10. 1 0
      src/core/vnotex.h
  11. 1 1
      src/core/widgetconfig.h
  12. 3 1
      src/data/core/core.qrc
  13. 14 0
      src/data/core/icons/buffer.svg
  14. 1 0
      src/data/core/icons/cancel.svg
  15. 0 1
      src/data/core/icons/saved.svg
  16. 10 0
      src/data/core/icons/search.svg
  17. 3 1
      src/data/core/vnotex.json
  18. 18 0
      src/data/extra/themes/moonlight/interface.qss
  19. 5 0
      src/data/extra/themes/moonlight/palette.json
  20. 5 0
      src/data/extra/themes/native/palette.json
  21. 18 0
      src/data/extra/themes/pure/interface.qss
  22. 5 0
      src/data/extra/themes/pure/palette.json
  23. 4 3
      src/main.cpp
  24. 259 0
      src/search/filesearchengine.cpp
  25. 98 0
      src/search/filesearchengine.h
  26. 57 0
      src/search/isearchengine.h
  27. 17 0
      src/search/search.pri
  28. 47 0
      src/search/searchdata.cpp
  29. 110 0
      src/search/searchdata.h
  30. 501 0
      src/search/searcher.cpp
  31. 93 0
      src/search/searcher.h
  32. 54 0
      src/search/searchresultitem.cpp
  33. 42 0
      src/search/searchresultitem.h
  34. 247 0
      src/search/searchtoken.cpp
  35. 86 0
      src/search/searchtoken.h
  36. 2 0
      src/src.pro
  37. 8 0
      src/utils/widgetutils.cpp
  38. 3 0
      src/utils/widgetutils.h
  39. 1 1
      src/widgets/attachmentpopup.cpp
  40. 2 2
      src/widgets/dialogs/dialog.cpp
  41. 2 2
      src/widgets/dialogs/exportdialog.cpp
  42. 1 1
      src/widgets/dialogs/managenotebooksdialog.cpp
  43. 1 1
      src/widgets/dialogs/scrolldialog.cpp
  44. 2 2
      src/widgets/findandreplacewidget.cpp
  45. 1 1
      src/widgets/findandreplacewidget.h
  46. 180 0
      src/widgets/locationlist.cpp
  47. 74 0
      src/widgets/locationlist.h
  48. 93 29
      src/widgets/mainwindow.cpp
  49. 23 1
      src/widgets/mainwindow.h
  50. 5 5
      src/widgets/markdownviewwindow.cpp
  51. 2 1
      src/widgets/notebookexplorer.cpp
  52. 1 1
      src/widgets/notebooknodeexplorer.cpp
  53. 3 0
      src/widgets/outlinepopup.cpp
  54. 4 3
      src/widgets/outlineviewer.cpp
  55. 1 1
      src/widgets/outlineviewer.h
  56. 10 8
      src/widgets/propertydefs.cpp
  57. 10 8
      src/widgets/propertydefs.h
  58. 43 0
      src/widgets/searchinfoprovider.cpp
  59. 36 0
      src/widgets/searchinfoprovider.h
  60. 526 0
      src/widgets/searchpanel.cpp
  61. 144 0
      src/widgets/searchpanel.h
  62. 29 6
      src/widgets/titlebar.cpp
  63. 12 2
      src/widgets/titlebar.h
  64. 1 1
      src/widgets/titletoolbar.cpp
  65. 5 4
      src/widgets/toolbarhelper.cpp
  66. 24 4
      src/widgets/viewarea.cpp
  67. 6 1
      src/widgets/viewarea.h
  68. 4 4
      src/widgets/viewsplit.cpp
  69. 1 1
      src/widgets/viewsplit.h
  70. 2 2
      src/widgets/viewwindow.cpp
  71. 4 4
      src/widgets/viewwindowtoolbarhelper.cpp
  72. 6 0
      src/widgets/widgets.pri
  73. 6 0
      src/widgets/widgetsfactory.cpp
  74. 3 0
      src/widgets/widgetsfactory.h
  75. 1 0
      tests/test_core/test_notebook/test_notebook.pro

+ 17 - 9
src/commandlineoptions.cpp

@@ -5,28 +5,36 @@
 #include <QCoreApplication>
 #include <QDebug>
 
-#define TR(x) QCoreApplication::translate("main", (x))
+#include <widgets/mainwindow.h>
+
+using vnotex::MainWindow;
 
 CommandLineOptions::ParseResult CommandLineOptions::parse(const QStringList &p_arguments)
 {
     QCommandLineParser parser;
-    parser.setApplicationDescription(TR("A pleasant note-taking platform."));
+    parser.setApplicationDescription(MainWindow::tr("A pleasant note-taking platform."));
     const auto helpOpt = parser.addHelpOption();
     const auto versionOpt = parser.addVersionOption();
 
     // Positional arguments.
-    parser.addPositionalArgument("paths", TR("Files or folders to open."));
+    parser.addPositionalArgument("paths", MainWindow::tr("Files or folders to open."));
 
-    const QCommandLineOption verboseOpt("verbose", TR("Print more logs."));
+    const QCommandLineOption verboseOpt("verbose", MainWindow::tr("Print more logs."));
     parser.addOption(verboseOpt);
 
     // WebEngine options.
     // No need to handle them. Just add them to the parser to avoid parse error.
-    QCommandLineOption webRemoteDebuggingPortOpt("remote-debugging-port",
-                                                 TR("WebEngine remote debugging port."),
-                                                 "port_number");
-    webRemoteDebuggingPortOpt.setFlags(QCommandLineOption::HiddenFromHelp);
-    parser.addOption(webRemoteDebuggingPortOpt);
+    {
+        QCommandLineOption webRemoteDebuggingPortOpt("remote-debugging-port",
+                                                     MainWindow::tr("WebEngine remote debugging port."),
+                                                     "port_number");
+        webRemoteDebuggingPortOpt.setFlags(QCommandLineOption::HiddenFromHelp);
+        parser.addOption(webRemoteDebuggingPortOpt);
+
+        QCommandLineOption webNoSandboxOpt("no-sandbox", MainWindow::tr("WebEngine without sandbox."));
+        webNoSandboxOpt.setFlags(QCommandLineOption::HiddenFromHelp);
+        parser.addOption(webNoSandboxOpt);
+    }
 
     if (!parser.parse(p_arguments)) {
         m_errorMsg = parser.errorText();

+ 1 - 0
src/core/core.pri

@@ -42,6 +42,7 @@ HEADERS += \
     $$PWD/filelocator.h \
     $$PWD/fileopenparameters.h \
     $$PWD/htmltemplatehelper.h \
+    $$PWD/location.h \
     $$PWD/logger.h \
     $$PWD/mainconfig.h \
     $$PWD/markdowneditorconfig.h \

+ 2 - 0
src/core/coreconfig.h

@@ -23,6 +23,8 @@ namespace vnotex
             CloseTab,
             NavigationDock,
             OutlineDock,
+            SearchDock,
+            LocationListDock,
             NavigationMode,
             LocateNode,
             VerticalSplit,

+ 3 - 0
src/core/fileopenparameters.h

@@ -29,6 +29,9 @@ namespace vnotex
 
         // Open as read-only.
         bool m_readOnly = false;
+
+        // If m_lineNumber > -1, it indicates the line to scroll to after opening the file.
+        int m_lineNumber = -1;
     };
 }
 

+ 4 - 2
src/core/global.h

@@ -64,12 +64,14 @@ namespace vnotex
 
     enum FindOption
     {
-        None = 0,
+        FindNone = 0,
         FindBackward = 0x1U,
         CaseSensitive = 0x2U,
         WholeWordOnly = 0x4U,
         RegularExpression = 0x8U,
-        IncrementalSearch = 0x10U
+        IncrementalSearch = 0x10U,
+        // Used in full-text search.
+        FuzzySearch = 0x20U
     };
     Q_DECLARE_FLAGS(FindOptions, FindOption);
 

+ 72 - 0
src/core/location.h

@@ -0,0 +1,72 @@
+#ifndef LOCATION_H
+#define LOCATION_H
+
+#include <QDebug>
+
+namespace vnotex
+{
+    struct Location
+    {
+        friend QDebug operator<<(QDebug p_dbg, const Location &p_loc)
+        {
+            QDebugStateSaver saver(p_dbg);
+            p_dbg.nospace() << p_loc.m_path << ":" << p_loc.m_lineNumber;
+            return p_dbg;
+        }
+
+        // TODO: support encoding like buffer/notebook.
+        QString m_path;
+
+        QString m_displayPath;
+
+        int m_lineNumber = -1;
+    };
+
+    enum class LocationType
+    {
+        Buffer,
+        File,
+        Folder,
+        Notebook
+    };
+
+    struct ComplexLocation
+    {
+        struct Line
+        {
+            Line() = default;
+
+            Line(int p_lineNumber, const QString &p_text)
+                : m_lineNumber(p_lineNumber),
+                  m_text(p_text)
+            {
+            }
+
+            int m_lineNumber = -1;
+
+            QString m_text;
+        };
+
+        void addLine(int p_lineNumber, const QString &p_text)
+        {
+            m_lines.push_back(Line(p_lineNumber, p_text));
+        }
+
+        friend QDebug operator<<(QDebug p_dbg, const ComplexLocation &p_loc)
+        {
+            QDebugStateSaver saver(p_dbg);
+            p_dbg.nospace() << static_cast<int>(p_loc.m_type) << p_loc.m_path << p_loc.m_displayPath;
+            return p_dbg;
+        }
+
+        LocationType m_type = LocationType::File;
+
+        QString m_path;
+
+        QString m_displayPath;
+
+        QVector<Line> m_lines;
+    };
+}
+
+#endif // LOCATION_H

+ 13 - 0
src/core/sessionconfig.cpp

@@ -62,6 +62,8 @@ void SessionConfig::init()
     }
 
     m_exportOption.fromJson(sessionJobj[QStringLiteral("export_option")].toObject());
+
+    m_searchOption.fromJson(sessionJobj[QStringLiteral("search_option")].toObject());
 }
 
 void SessionConfig::loadCore(const QJsonObject &p_session)
@@ -177,6 +179,7 @@ QJsonObject SessionConfig::toJson() const
     obj[QStringLiteral("notebooks")] = saveNotebooks();
     obj[QStringLiteral("state_geometry")] = saveStateAndGeometry();
     obj[QStringLiteral("export_option")] = m_exportOption.toJson();
+    obj[QStringLiteral("search_option")] = m_searchOption.toJson();
     return obj;
 }
 
@@ -289,6 +292,16 @@ void SessionConfig::setExportOption(const ExportOption &p_option)
     updateConfig(m_exportOption, p_option, this);
 }
 
+const SearchOption &SessionConfig::getSearchOption() const
+{
+    return m_searchOption;
+}
+
+void SessionConfig::setSearchOption(const SearchOption &p_option)
+{
+    updateConfig(m_searchOption, p_option, this);
+}
+
 void SessionConfig::loadStateAndGeometry(const QJsonObject &p_session)
 {
     const auto obj = p_session.value(QStringLiteral("state_geometry")).toObject();

+ 6 - 0
src/core/sessionconfig.h

@@ -7,6 +7,7 @@
 #include <QVector>
 
 #include <export/exportdata.h>
+#include <search/searchdata.h>
 
 namespace vnotex
 {
@@ -87,6 +88,9 @@ namespace vnotex
         const ExportOption &getExportOption() const;
         void setExportOption(const ExportOption &p_option);
 
+        const SearchOption &getSearchOption() const;
+        void setSearchOption(const SearchOption &p_option);
+
     private:
         void loadCore(const QJsonObject &p_session);
 
@@ -123,6 +127,8 @@ namespace vnotex
         int m_minimizeToSystemTray = -1;
 
         ExportOption m_exportOption;
+
+        SearchOption m_searchOption;
     };
 } // ns vnotex
 

+ 1 - 0
src/core/vnotex.cpp

@@ -8,6 +8,7 @@
 #include "buffermgr.h"
 #include "configmgr.h"
 #include "coreconfig.h"
+#include "location.h"
 
 #include "fileopenparameters.h"
 

+ 1 - 0
src/core/vnotex.h

@@ -16,6 +16,7 @@ namespace vnotex
     struct FileOpenParameters;
     class Event;
     class Notebook;
+    struct ComplexLocation;
 
     class VNoteX : public QObject
     {

+ 1 - 1
src/core/widgetconfig.h

@@ -39,7 +39,7 @@ namespace vnotex
     private:
         int m_outlineAutoExpandedLevel = 6;
 
-        FindOptions m_findAndReplaceOptions = FindOption::None;
+        FindOptions m_findAndReplaceOptions = FindOption::FindNone;
 
         int m_nodeExplorerViewOrder = 0;
 

+ 3 - 1
src/data/core/core.qrc

@@ -35,8 +35,8 @@
         <file>icons/remove_notebook.svg</file>
         <file>icons/close_notebook.svg</file>
         <file>icons/recycle_bin.svg</file>
-        <file>icons/saved.svg</file>
         <file>icons/save_editor.svg</file>
+        <file>icons/buffer.svg</file>
         <file>icons/attachment_editor.svg</file>
         <file>icons/attachment_full_editor.svg</file>
         <file>icons/split_menu.svg</file>
@@ -74,6 +74,8 @@
         <file>icons/stay_on_top.svg</file>
         <file>icons/outline_editor.svg</file>
         <file>icons/find_replace_editor.svg</file>
+        <file>icons/search.svg</file>
+        <file>icons/cancel.svg</file>
         <file>icons/section_number_editor.svg</file>
         <file>icons/sort.svg</file>
         <file>logo/vnote.svg</file>

+ 14 - 0
src/data/core/icons/buffer.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="M112,64v16v320h16V80h304v337.143c0,8.205-6.652,14.857-14.857,14.857H94.857C86.652,432,80,425.348,80,417.143V128h16v-16
+		H64v305.143C64,434.157,77.843,448,94.857,448h322.285C434.157,448,448,434.157,448,417.143V64H112z"/>
+	<rect style="fill:#000000" x="160" y="112" width="128" height="16"/>
+	<rect style="fill:#000000" x="160" y="192" width="240" height="16"/>
+	<rect style="fill:#000000" x="160" y="272" width="192" height="16"/>
+	<rect style="fill:#000000" x="160" y="352" width="240" height="16"/>
+</g>
+</svg>

+ 1 - 0
src/data/core/icons/cancel.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1616726105805" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1116" width="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372 0-89 31.3-170.8 83.5-234.8l523.3 523.3C682.8 852.7 601 884 512 884z m288.5-137.2L277.2 223.5C341.2 171.3 423 140 512 140c205.4 0 372 166.6 372 372 0 89-31.3 170.8-83.5 234.8z" p-id="1117" fill="#000000"></path></svg>

+ 0 - 1
src/data/core/icons/saved.svg

@@ -1 +0,0 @@
-<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1609209772514" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3065" width="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M819.2 104.96H168.96c-45.1712 0-81.92 36.7488-81.92 81.92v650.24c0 45.1712 36.7488 81.92 81.92 81.92h650.24c45.1712 0 81.92-36.7488 81.92-81.92V186.88c0-45.1712-36.7488-81.92-81.92-81.92zM488.96 207.36v79.36c0 25.44896 20.63104 46.08 46.08 46.08s46.08-20.63104 46.08-46.08V207.36h43.52v176.64H366.08V207.36h122.88z m309.76 609.28H189.44V207.36h74.24v197.12c0 45.1712 36.7488 81.92 81.92 81.92h299.52c45.1712 0 81.92-36.7488 81.92-81.92V207.36h71.68v609.28z" p-id="3066" fill="#000000"></path></svg>

+ 10 - 0
src/data/core/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>

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

@@ -17,6 +17,8 @@
             "CloseTab" : "Ctrl+G, X",
             "NavigationDock" : "Ctrl+G, 1",
             "OutlineDock" : "Ctrl+G, 2",
+            "SearchDock" : "Ctrl+G, 3",
+            "LocationListDock" : "Ctrl+G, 4",
             "NavigationMode" : "Ctrl+G, W",
             "LocateNode" : "Ctrl+G, D",
             "VerticalSplit" : "Ctrl+G, \\",
@@ -260,7 +262,7 @@
             "//comment" : "Whether insert the file name as title on new file",
             "insert_file_name_as_title" : true,
             "//comment" : "none/read/edit",
-            "section_number" : "read",
+            "section_number" : "none",
             "//comment" : "Base level to start section numbering",
             "section_number_base_level" : 2,
             "//comment" : "Style of the section number in edit mode",

+ 18 - 0
src/data/extra/themes/moonlight/interface.qss

@@ -412,6 +412,24 @@ vnotex--MainWindow QLabel#MainWindowTipsLabel {
 }
 
 /* QLineEdit */
+QLineEdit[EmbeddedLineEdit="true"] {
+    border: none;
+    padding: 0px;
+    margin: 0px;
+    color: @widgets#qlineedit#fg;
+    background-color: transparent;
+}
+
+QLineEdit[EmbeddedLineEdit="true"]:focus {
+    border: none;
+    background-color: @widgets#qlineedit#focus#bg;
+}
+
+QLineEdit[EmbeddedLineEdit="true"]:hover {
+    border: none;
+    background-color: @widgets#qlineedit#hover#bg;
+}
+
 QLineEdit {
     border: 1px solid @widgets#qlineedit#border;
     padding: 3px;

+ 5 - 0
src/data/extra/themes/moonlight/palette.json

@@ -233,6 +233,11 @@
                 "fg" : "@base#icon#inactive#fg"
             }
         },
+        "locationlist" : {
+            "node_icon" : {
+                "fg" : "@base#icon#fg"
+            }
+        },
         "viewsplit" : {
             "action_button" : {
                 "fg" : "@base#icon#inactive#fg",

+ 5 - 0
src/data/extra/themes/native/palette.json

@@ -92,6 +92,11 @@
                 "fg" : "@base#icon#disabled#fg"
             }
         },
+        "locationlist" : {
+            "node_icon" : {
+                "fg" : "@base#icon#fg"
+            }
+        },
         "viewsplit" : {
             "action_button" : {
                 "fg" : "#808080",

+ 18 - 0
src/data/extra/themes/pure/interface.qss

@@ -412,6 +412,24 @@ vnotex--MainWindow QLabel#MainWindowTipsLabel {
 }
 
 /* QLineEdit */
+QLineEdit[EmbeddedLineEdit="true"] {
+    border: none;
+    padding: 0px;
+    margin: 0px;
+    color: @widgets#qlineedit#fg;
+    background-color: transparent;
+}
+
+QLineEdit[EmbeddedLineEdit="true"]:focus {
+    border: none;
+    background-color: @widgets#qlineedit#focus#bg;
+}
+
+QLineEdit[EmbeddedLineEdit="true"]:hover {
+    border: none;
+    background-color: @widgets#qlineedit#hover#bg;
+}
+
 QLineEdit {
     border: 1px solid @widgets#qlineedit#border;
     padding: 3px;

+ 5 - 0
src/data/extra/themes/pure/palette.json

@@ -229,6 +229,11 @@
                 "fg" : "@base#icon#inactive#fg"
             }
         },
+        "locationlist" : {
+            "node_icon" : {
+                "fg" : "@base#icon#fg"
+            }
+        },
         "viewsplit" : {
             "action_button" : {
                 "fg" : "@base#icon#inactive#fg",

+ 4 - 3
src/main.cpp

@@ -89,8 +89,9 @@ int main(int argc, char *argv[])
         break;
 
     case CommandLineOptions::Error:
-        showMessageOnCommandLineIfAvailable(cmdOptions.m_errorMsg);
-        return -1;
+        fprintf(stderr, "%s\n", qPrintable(cmdOptions.m_errorMsg));
+        // Arguments to WebEngineView will be unknown ones. So just let it go.
+        break;
 
     case CommandLineOptions::VersionRequested:
     {
@@ -244,6 +245,6 @@ void showMessageOnCommandLineIfAvailable(const QString &p_msg)
     MessageBoxHelper::notify(MessageBoxHelper::Information,
                              QString("<pre>%1</pre>").arg(p_msg));
 #else
-    printf("%s\n", qPrintable(p_msg));
+    fprintf(stderr, "%s\n", qPrintable(p_msg));
 #endif
 }

+ 259 - 0
src/search/filesearchengine.cpp

@@ -0,0 +1,259 @@
+#include "filesearchengine.h"
+
+#include <QFile>
+#include <QMimeDatabase>
+#include <QDebug>
+
+#include "searchresultitem.h"
+
+using namespace vnotex;
+
+FileSearchEngineWorker::FileSearchEngineWorker(QObject *p_parent)
+    : QThread(p_parent)
+{
+}
+
+void FileSearchEngineWorker::setData(const QVector<SearchSecondPhaseItem> &p_items,
+                                     const QSharedPointer<SearchOption> &p_option,
+                                     const SearchToken &p_token)
+{
+    m_items = p_items;
+    m_option = p_option;
+    m_token = p_token;
+}
+
+void FileSearchEngineWorker::stop()
+{
+    m_askedToStop.store(1);
+}
+
+bool FileSearchEngineWorker::isAskedToStop() const
+{
+    return m_askedToStop.load() == 1;
+}
+
+void FileSearchEngineWorker::run()
+{
+    const int c_batchSize = 100;
+
+    QMimeDatabase mimeDatabase;
+    m_state = SearchState::Busy;
+
+    m_results.clear();
+    int nr = 0;
+    for (const auto &item : m_items) {
+        if (isAskedToStop()) {
+            m_state = SearchState::Stopped;
+            break;
+        }
+
+        const QMimeType mimeType = mimeDatabase.mimeTypeForFile(item.m_filePath);
+        if (mimeType.isValid() && !mimeType.inherits(QStringLiteral("text/plain"))) {
+            appendError(tr("Skip binary file (%1)").arg(item.m_filePath));
+            continue;
+        }
+
+        searchFile(item.m_filePath, item.m_displayPath);
+
+        if (++nr >= c_batchSize) {
+            nr = 0;
+            processBatchResults();
+        }
+    }
+
+    processBatchResults();
+
+    if (m_state == SearchState::Busy) {
+        m_state = SearchState::Finished;
+    }
+}
+
+void FileSearchEngineWorker::appendError(const QString &p_err)
+{
+    m_errors.append(p_err);
+}
+
+void FileSearchEngineWorker::searchFile(const QString &p_filePath, const QString &p_displayPath)
+{
+    QFile file(p_filePath);
+    if (!file.open(QIODevice::ReadOnly)) {
+        return;
+    }
+
+    const bool shouldStartBatchMode = m_token.shouldStartBatchMode();
+    if (shouldStartBatchMode) {
+        m_token.startBatchMode();
+    }
+
+    QSharedPointer<SearchResultItem> resultItem;
+
+    int lineNum = 1;
+    QTextStream ins(&file);
+    while (!ins.atEnd()) {
+        if (isAskedToStop()) {
+            m_state = SearchState::Stopped;
+            break;
+        }
+
+        const auto lineText = ins.readLine();
+        bool matched = false;
+        if (!shouldStartBatchMode) {
+            matched = m_token.matched(lineText);
+        } else {
+            matched = m_token.matchedInBatchMode(lineText);
+        }
+
+        if (matched) {
+            if (resultItem) {
+                resultItem->addLine(lineNum, lineText);
+            } else {
+                resultItem = SearchResultItem::createFileItem(p_filePath, p_displayPath, lineNum, lineText);
+            }
+        }
+
+        if (shouldStartBatchMode && m_token.readyToEndBatchMode()) {
+            break;
+        }
+
+        ++lineNum;
+    }
+
+    if (shouldStartBatchMode) {
+        bool allMatched = m_token.readyToEndBatchMode();
+        m_token.endBatchMode();
+
+        if (!allMatched) {
+            // This file does not meet all the tokens.
+            resultItem.reset();
+        }
+    }
+
+    if (resultItem) {
+        m_results.append(resultItem);
+    }
+}
+
+void FileSearchEngineWorker::processBatchResults()
+{
+    if (!m_results.isEmpty()) {
+        emit resultItemsReady(m_results);
+        m_results.clear();
+    }
+}
+
+FileSearchEngine::FileSearchEngine()
+{
+}
+
+FileSearchEngine::~FileSearchEngine()
+{
+    stopInternal();
+    clearInternal();
+}
+
+void FileSearchEngine::search(const QSharedPointer<SearchOption> &p_option,
+                              const SearchToken &p_token,
+                              const QVector<SearchSecondPhaseItem> &p_items)
+{
+    int numThread = QThread::idealThreadCount();
+    if (numThread < 1) {
+        numThread = 1;
+    }
+
+    Q_ASSERT(!p_items.isEmpty());
+    if (p_items.size() < numThread) {
+        numThread = 1;
+    }
+
+    clearWorkers();
+    m_workers.reserve(numThread);
+    const int totalSize = p_items.size();
+    const int step = totalSize / numThread;
+    int remain = totalSize % numThread;
+    int start = 0;
+    for (int i = 0; i < numThread && start < totalSize; ++i) {
+        int len = step;
+        if (remain) {
+            ++len;
+            --remain;
+        }
+
+        if (start + len > totalSize) {
+            len = totalSize - start;
+        }
+
+        auto th = QSharedPointer<FileSearchEngineWorker>::create();
+        th->setData(p_items.mid(start, len), p_option, p_token);
+        connect(th.data(), &FileSearchEngineWorker::finished,
+                this, &FileSearchEngine::handleWorkerFinished);
+        connect(th.data(), &FileSearchEngineWorker::resultItemsReady,
+                this, &FileSearchEngine::resultItemsAdded);
+
+        m_workers.append(th);
+        th->start();
+
+        start += len;
+    }
+}
+
+void FileSearchEngine::stop()
+{
+    stopInternal();
+}
+
+void FileSearchEngine::stopInternal()
+{
+    for (const auto &th : m_workers) {
+        th->stop();
+    }
+}
+
+void FileSearchEngine::clear()
+{
+    clearInternal();
+}
+
+void FileSearchEngine::clearInternal()
+{
+    clearWorkers();
+}
+
+void FileSearchEngine::clearWorkers()
+{
+    for (const auto &th : m_workers) {
+        th->quit();
+        th->wait();
+    }
+
+    m_workers.clear();
+    m_numOfFinishedWorkers = 0;
+}
+
+void FileSearchEngine::handleWorkerFinished()
+{
+    ++m_numOfFinishedWorkers;
+    if (m_numOfFinishedWorkers == m_workers.size()) {
+        SearchState state = SearchState::Finished;
+
+        for (const auto &th : m_workers) {
+            if (th->m_state == SearchState::Failed) {
+                if (state != SearchState::Stopped) {
+                    state = SearchState::Failed;
+                }
+            } else if (th->m_state == SearchState::Stopped) {
+                state = SearchState::Stopped;
+            }
+
+            for (const auto &err : th->m_errors) {
+                emit logRequested(err);
+            }
+
+            Q_ASSERT(th->isFinished());
+        }
+
+        m_workers.clear();
+        m_numOfFinishedWorkers = 0;
+
+        emit finished(state);
+    }
+}

+ 98 - 0
src/search/filesearchengine.h

@@ -0,0 +1,98 @@
+#ifndef SEARCHENGINE_H
+#define SEARCHENGINE_H
+
+#include "isearchengine.h"
+
+#include <QThread>
+#include <QRegularExpression>
+#include <QAtomicInt>
+#include <QVector>
+
+#include "searchtoken.h"
+#include "searchdata.h"
+
+namespace vnotex
+{
+    struct SearchResultItem;
+
+    class FileSearchEngineWorker : public QThread
+    {
+        Q_OBJECT
+        friend class FileSearchEngine;
+    public:
+        explicit FileSearchEngineWorker(QObject *p_parent = nullptr);
+
+        ~FileSearchEngineWorker() = default;
+
+        void setData(const QVector<SearchSecondPhaseItem> &p_items,
+                     const QSharedPointer<SearchOption> &p_option,
+                     const SearchToken &p_token);
+
+    public slots:
+        void stop();
+
+    signals:
+        void resultItemsReady(const QVector<QSharedPointer<SearchResultItem>> &p_items);
+
+    protected:
+        void run() Q_DECL_OVERRIDE;
+
+    private:
+        void appendError(const QString &p_err);
+
+        void searchFile(const QString &p_filePath, const QString &p_displayPath);
+
+        void processBatchResults();
+
+        bool isAskedToStop() const;
+
+        QAtomicInt m_askedToStop = 0;
+
+        QVector<SearchSecondPhaseItem> m_items;
+
+        SearchToken m_token;
+
+        QSharedPointer<SearchOption> m_option;
+
+        SearchState m_state = SearchState::Idle;
+
+        QStringList m_errors;
+
+        QVector<QSharedPointer<SearchResultItem>> m_results;
+    };
+
+    class FileSearchEngine : public ISearchEngine
+    {
+        Q_OBJECT
+    public:
+        FileSearchEngine();
+
+        ~FileSearchEngine();
+
+        void search(const QSharedPointer<SearchOption> &p_option,
+                    const SearchToken &p_token,
+                    const QVector<SearchSecondPhaseItem> &p_items);
+
+        void stop() Q_DECL_OVERRIDE;
+
+        void clear() Q_DECL_OVERRIDE;
+
+    private slots:
+        void handleWorkerFinished();
+
+    private:
+        void clearWorkers();
+
+        // Need non-virtual version of this.
+        void stopInternal();
+
+        // Need non-virtual version of this.
+        void clearInternal();
+
+        int m_numOfFinishedWorkers = 0;
+
+        QVector<QSharedPointer<FileSearchEngineWorker>> m_workers;
+    };
+}
+
+#endif // SEARCHENGINE_H

+ 57 - 0
src/search/isearchengine.h

@@ -0,0 +1,57 @@
+#ifndef ISEARCHENGINE_H
+#define ISEARCHENGINE_H
+
+#include <QObject>
+#include <QVector>
+#include <QList>
+#include <QSharedPointer>
+
+#include "searchdata.h"
+
+namespace vnotex
+{
+    struct SearchResultItem;
+
+    class SearchToken;
+
+    struct SearchSecondPhaseItem
+    {
+        SearchSecondPhaseItem() = default;
+
+        SearchSecondPhaseItem(const QString &p_filePath, const QString &p_displayPath)
+            : m_filePath(p_filePath),
+              m_displayPath(p_displayPath)
+        {
+        }
+
+        QString m_filePath;
+
+        QString m_displayPath;
+    };
+
+    class ISearchEngine : public QObject
+    {
+        Q_OBJECT
+    public:
+        ISearchEngine() = default;
+
+        virtual ~ISearchEngine() = default;
+
+        virtual void search(const QSharedPointer<SearchOption> &p_option,
+                            const SearchToken &p_token,
+                            const QVector<SearchSecondPhaseItem> &p_items) = 0;
+
+        virtual void stop() = 0;
+
+        virtual void clear() = 0;
+
+    signals:
+        void finished(SearchState p_state);
+
+        void resultItemsAdded(const QVector<QSharedPointer<SearchResultItem>> &p_items);
+
+        void logRequested(const QString &p_log);
+    };
+}
+
+#endif // ISEARCHENGINE_H

+ 17 - 0
src/search/search.pri

@@ -0,0 +1,17 @@
+QT += widgets
+
+HEADERS += \
+    $$PWD/filesearchengine.h \
+    $$PWD/isearchengine.h \
+    $$PWD/searchdata.h \
+    $$PWD/searcher.h \
+    $$PWD/searchresultitem.h \
+    $$PWD/searchtoken.h
+
+SOURCES += \
+    $$PWD/filesearchengine.cpp \
+    $$PWD/searchdata.cpp \
+    $$PWD/searcher.cpp \
+    $$PWD/searchresultitem.cpp \
+    $$PWD/searchtoken.cpp
+

+ 47 - 0
src/search/searchdata.cpp

@@ -0,0 +1,47 @@
+#include "searchdata.h"
+
+#include <QJsonObject>
+
+using namespace vnotex;
+
+SearchOption::SearchOption()
+    : m_objects(SearchObject::SearchName | SearchObject::SearchContent),
+      m_targets(SearchTarget::SearchFile | SearchTarget::SearchFolder)
+{
+}
+
+QJsonObject SearchOption::toJson() const
+{
+    QJsonObject obj;
+    obj["file_pattern"] = m_filePattern;
+    obj["scope"] = static_cast<int>(m_scope);
+    obj["objects"] = static_cast<int>(m_objects);
+    obj["targets"] = static_cast<int>(m_targets);
+    obj["engine"] = static_cast<int>(m_engine);
+    obj["find_options"] = static_cast<int>(m_findOptions);
+    return obj;
+}
+
+void SearchOption::fromJson(const QJsonObject &p_obj)
+{
+    if (p_obj.isEmpty()) {
+        return;
+    }
+
+    m_filePattern = p_obj["file_pattern"].toString();
+    m_scope = static_cast<SearchScope>(p_obj["scope"].toInt());
+    m_objects = static_cast<SearchObjects>(p_obj["objects"].toInt());
+    m_targets = static_cast<SearchTargets>(p_obj["targets"].toInt());
+    m_engine = static_cast<SearchEngine>(p_obj["engine"].toInt());
+    m_findOptions = static_cast<FindOptions>(p_obj["find_options"].toInt());
+}
+
+bool SearchOption::operator==(const SearchOption &p_other) const
+{
+    return m_filePattern == p_other.m_filePattern
+           && m_scope == p_other.m_scope
+           && m_objects == p_other.m_objects
+           && m_targets == p_other.m_targets
+           && m_engine == p_other.m_engine
+           && m_findOptions == p_other.m_findOptions;
+}

+ 110 - 0
src/search/searchdata.h

@@ -0,0 +1,110 @@
+#ifndef SEARCHOPTION_H
+#define SEARCHOPTION_H
+
+#include <QObject>
+#include <QString>
+
+#include <core/global.h>
+
+namespace vnotex
+{
+    enum class SearchState
+    {
+        Idle = 0,
+        Busy,
+        Finished,
+        Failed,
+        Stopped
+    };
+
+    class SearchTranslate : public QObject
+    {
+        Q_OBJECT
+    };
+
+    inline QString SearchStateToString(SearchState p_state)
+    {
+        switch (p_state) {
+        case SearchState::Idle:
+            return SearchTranslate::tr("Idle");
+
+        case SearchState::Busy:
+            return SearchTranslate::tr("Busy");
+
+        case SearchState::Finished:
+            return SearchTranslate::tr("Finished");
+
+        case SearchState::Failed:
+            return SearchTranslate::tr("Failed");
+
+        case SearchState::Stopped:
+            return SearchTranslate::tr("Stopped");
+        }
+
+        return QString();
+    }
+
+    enum class SearchScope
+    {
+        Buffers = 0,
+        CurrentFolder,
+        CurrentNotebook,
+        AllNotebooks
+    };
+
+    enum SearchObject
+    {
+        ObjectNone = 0,
+        SearchName = 0x1UL,
+        SearchContent = 0x2UL,
+        SearchOutline = 0x4UL,
+        SearchTag = 0x8UL,
+        SearchPath = 0x10UL
+    };
+    Q_DECLARE_FLAGS(SearchObjects, SearchObject);
+
+    enum SearchTarget
+    {
+        TargetNone = 0,
+        SearchFile = 0x1UL,
+        SearchFolder = 0x2UL,
+        SearchNotebook = 0x4UL
+    };
+    Q_DECLARE_FLAGS(SearchTargets, SearchTarget);
+
+    enum class SearchEngine
+    {
+        Internal = 0
+    };
+
+    struct SearchOption
+    {
+        SearchOption();
+
+        QJsonObject toJson() const;
+        void fromJson(const QJsonObject &p_obj);
+
+        bool operator==(const SearchOption &p_other) const;
+
+        QString m_keyword;
+
+        QString m_filePattern;
+
+        SearchScope m_scope = SearchScope::CurrentNotebook;
+
+        // *nix requests to init in the constructor.
+        SearchObjects m_objects;
+
+        // *nix requests to init in the constructor.
+        SearchTargets m_targets;
+
+        SearchEngine m_engine = SearchEngine::Internal;
+
+        FindOptions m_findOptions = FindOption::FindNone;
+    };
+}
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::SearchObjects);
+Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::SearchTargets);
+
+#endif // SEARCHOPTION_H

+ 501 - 0
src/search/searcher.cpp

@@ -0,0 +1,501 @@
+#include "searcher.h"
+
+#include <QCoreApplication>
+#include <QDebug>
+
+#include <buffer/buffer.h>
+#include <core/file.h>
+#include <notebook/node.h>
+#include <notebook/notebook.h>
+
+#include "searchresultitem.h"
+#include "filesearchengine.h"
+
+using namespace vnotex;
+
+Searcher::Searcher(QObject *p_parent)
+    : QObject(p_parent)
+{
+}
+
+void Searcher::clear()
+{
+    m_option.clear();
+
+    if (m_engine) {
+        m_engine->clear();
+        m_engine.reset();
+    }
+
+    m_askedToStop = false;
+}
+
+void Searcher::stop()
+{
+    m_askedToStop = true;
+
+    if (m_engine) {
+        m_engine->stop();
+    }
+}
+
+SearchState Searcher::search(const QSharedPointer<SearchOption> &p_option, const QList<Buffer *> &p_buffers)
+{
+    if (!(p_option->m_targets & SearchTarget::SearchFile)) {
+        // Only File target is applicable.
+        return SearchState::Finished;
+    }
+
+    if (!prepare(p_option)) {
+        return SearchState::Failed;
+    }
+
+    emit logRequested(tr("Searching %n buffer(s)", "", p_buffers.size()));
+
+    emit progressUpdated(0, p_buffers.size());
+    for (int i = 0; i < p_buffers.size(); ++i) {
+        if (!p_buffers[i]) {
+            continue;
+        }
+
+        if (isAskedToStop()) {
+            return SearchState::Stopped;
+        }
+
+        auto file = p_buffers[i]->getFile();
+        if (!firstPhaseSearch(file.data())) {
+            return SearchState::Failed;
+        }
+
+        emit progressUpdated(i + 1, p_buffers.size());
+    }
+
+    return SearchState::Finished;
+}
+
+SearchState Searcher::search(const QSharedPointer<SearchOption> &p_option, Node *p_folder)
+{
+    Q_ASSERT(p_folder->isContainer());
+    if (!(p_option->m_targets & (SearchTarget::SearchFile | SearchTarget::SearchFolder))) {
+        // Only File/Folder target is applicable.
+        return SearchState::Finished;
+    }
+
+    if (!prepare(p_option)) {
+        return SearchState::Failed;
+    }
+
+    emit logRequested(tr("Searching folder (%1)").arg(p_folder->getName()));
+
+    QVector<SearchSecondPhaseItem> secondPhaseItems;
+    if (!firstPhaseSearchFolder(p_folder, secondPhaseItems)) {
+        return SearchState::Failed;
+    }
+
+    if (isAskedToStop()) {
+        return SearchState::Stopped;
+    }
+
+    if (!secondPhaseItems.isEmpty()) {
+        // Do second phase search.
+        if (!secondPhaseSearch(secondPhaseItems)) {
+            return SearchState::Failed;
+        }
+
+        if (isAskedToStop()) {
+            return SearchState::Stopped;
+        }
+
+        return SearchState::Busy;
+    }
+
+    return SearchState::Finished;
+}
+
+SearchState Searcher::search(const QSharedPointer<SearchOption> &p_option, const QVector<Notebook *> &p_notebooks)
+{
+    if (!prepare(p_option)) {
+        return SearchState::Failed;
+    }
+
+    QVector<SearchSecondPhaseItem> secondPhaseItems;
+
+    emit progressUpdated(0, p_notebooks.size());
+    for (int i = 0; i < p_notebooks.size(); ++i) {
+        if (isAskedToStop()) {
+            return SearchState::Stopped;
+        }
+
+        emit logRequested(tr("Searching notebook (%1)").arg(p_notebooks[i]->getName()));
+
+        if (!firstPhaseSearch(p_notebooks[i], secondPhaseItems)) {
+            return SearchState::Failed;
+        }
+
+        emit progressUpdated(i + 1, p_notebooks.size());
+    }
+
+    if (isAskedToStop()) {
+        return SearchState::Stopped;
+    }
+
+    if (!secondPhaseItems.isEmpty()) {
+        // Do second phase search.
+        if (!secondPhaseSearch(secondPhaseItems)) {
+            return SearchState::Failed;
+        }
+
+        if (isAskedToStop()) {
+            return SearchState::Stopped;
+        }
+
+        return SearchState::Busy;
+    }
+
+    return SearchState::Finished;
+}
+
+bool Searcher::prepare(const QSharedPointer<SearchOption> &p_option)
+{
+    Q_ASSERT(!m_option);
+    m_option = p_option;
+
+    if (!SearchToken::compile(m_option->m_keyword, m_option->m_findOptions, m_token)) {
+        emit logRequested(tr("Failed to compile tokens (%1)").arg(m_option->m_keyword));
+        return false;
+    }
+
+    if (m_option->m_filePattern.isEmpty()) {
+        m_filePattern = QRegularExpression();
+    } else {
+        m_filePattern = QRegularExpression(QRegularExpression::wildcardToRegularExpression(m_option->m_filePattern), QRegularExpression::CaseInsensitiveOption);
+    }
+
+    return true;
+}
+
+bool Searcher::isAskedToStop() const
+{
+    QCoreApplication::sendPostedEvents();
+    return m_askedToStop;
+}
+
+static QString tryGetRelativePath(const File *p_file)
+{
+    const auto node = p_file->getNode();
+    if (node) {
+        return node->fetchPath();
+    }
+    return p_file->getFilePath();
+}
+
+bool Searcher::firstPhaseSearch(const File *p_file)
+{
+    if (!p_file) {
+        return true;
+    }
+
+    Q_ASSERT(testTarget(SearchTarget::SearchFile));
+
+    const auto name = p_file->getName();
+    if (!isFilePatternMatched(name)) {
+        return true;
+    }
+
+    const auto filePath = p_file->getFilePath();
+    const auto relativePath = tryGetRelativePath(p_file);
+
+    if (testObject(SearchObject::SearchName)) {
+        if (isTokenMatched(name)) {
+            emit resultItemAdded(SearchResultItem::createBufferItem(filePath, relativePath, -1, name));
+        }
+    }
+
+    if (testObject(SearchObject::SearchPath)) {
+        if (isTokenMatched(relativePath)) {
+            emit resultItemAdded(SearchResultItem::createBufferItem(filePath, relativePath, -1, name));
+        }
+    }
+
+    if (testObject(SearchObject::SearchOutline)) {
+        emit logRequested(tr("Searching outline is not supported yet"));
+    }
+
+    if (testObject(SearchObject::SearchTag)) {
+        emit logRequested(tr("Searching tag is not supported yet"));
+    }
+
+    // Make SearchContent always the last one to check.
+    if (testObject(SearchObject::SearchContent)) {
+        if (!searchContent(p_file)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool Searcher::isFilePatternMatched(const QString &p_name) const
+{
+    if (m_option->m_filePattern.isEmpty()) {
+        return true;
+    }
+
+    return m_filePattern.match(p_name).hasMatch();
+}
+
+bool Searcher::testTarget(SearchTarget p_target) const
+{
+    return m_option->m_targets & p_target;
+}
+
+bool Searcher::testObject(SearchObject p_object) const
+{
+    return m_option->m_objects & p_object;
+}
+
+bool Searcher::isTokenMatched(const QString &p_text) const
+{
+    return m_token.matched(p_text);
+}
+
+bool Searcher::searchContent(const File *p_file)
+{
+    const auto content = p_file->read();
+    if (content.isEmpty()) {
+        return true;
+    }
+
+    const bool shouldStartBatchMode = m_token.shouldStartBatchMode();
+    if (shouldStartBatchMode) {
+        m_token.startBatchMode();
+    }
+
+    const auto filePath = p_file->getFilePath();
+    const auto relativePath = tryGetRelativePath(p_file);
+
+    QSharedPointer<SearchResultItem> resultItem;
+
+    int lineNum = 1;
+    int pos = 0;
+    int contentSize = content.size();
+    QRegularExpression newlineRegExp("\\n|\\r\\n|\\r");
+    while (pos < contentSize) {
+        if (isAskedToStop()) {
+            break;
+        }
+
+        QRegularExpressionMatch match;
+        int idx = content.indexOf(newlineRegExp, pos, &match);
+        if (idx == -1) {
+            idx = contentSize;
+        }
+
+        if (idx > pos) {
+            QString lineText = content.mid(pos, idx - pos);
+            bool matched = false;
+            if (!shouldStartBatchMode) {
+                matched = m_token.matched(lineText);
+            } else {
+                matched = m_token.matchedInBatchMode(lineText);
+            }
+
+            if (matched) {
+                if (resultItem) {
+                    resultItem->addLine(lineNum, lineText);
+                } else {
+                    resultItem = SearchResultItem::createBufferItem(filePath, relativePath, lineNum, lineText);
+                }
+            }
+        }
+
+        if (idx == contentSize) {
+            break;
+        }
+
+        if (shouldStartBatchMode && m_token.readyToEndBatchMode()) {
+            break;
+        }
+
+        pos = idx + match.capturedLength();
+        ++lineNum;
+    }
+
+    if (shouldStartBatchMode) {
+        bool allMatched = m_token.readyToEndBatchMode();
+        m_token.endBatchMode();
+
+        if (!allMatched) {
+            // This file does not meet all the tokens.
+            resultItem.reset();
+        }
+    }
+
+    if (resultItem) {
+        emit resultItemAdded(resultItem);
+    }
+
+    return true;
+}
+
+bool Searcher::firstPhaseSearchFolder(Node *p_node, QVector<SearchSecondPhaseItem> &p_secondPhaseItems)
+{
+    if (!p_node) {
+        return true;
+    }
+
+    Q_ASSERT(p_node->isContainer());
+    Q_ASSERT(testTarget(SearchTarget::SearchFile) || testTarget(SearchTarget::SearchFolder));
+
+    p_node->load();
+
+    if (testTarget(SearchTarget::SearchFolder)) {
+        const auto name = p_node->getName();
+        const auto folderPath = p_node->fetchAbsolutePath();
+        const auto relativePath = p_node->fetchPath();
+        if (testObject(SearchObject::SearchName)) {
+            if (isTokenMatched(name)) {
+                emit resultItemAdded(SearchResultItem::createFolderItem(folderPath, relativePath));
+            }
+        }
+
+        if (testObject(SearchObject::SearchPath)) {
+            if (isTokenMatched(relativePath)) {
+                emit resultItemAdded(SearchResultItem::createFolderItem(folderPath, relativePath));
+            }
+        }
+    }
+
+    // Search children.
+    const auto &children = p_node->getChildrenRef();
+    for (const auto &child : children) {
+        if (isAskedToStop()) {
+            return true;
+        }
+
+        if (child->hasContent() && testTarget(SearchTarget::SearchFile)) {
+            if (!firstPhaseSearch(child.data(), p_secondPhaseItems)) {
+                return false;
+            }
+        }
+
+        if (child->isContainer()) {
+            if (!firstPhaseSearchFolder(child.data(), p_secondPhaseItems)) {
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+bool Searcher::firstPhaseSearch(Node *p_node, QVector<SearchSecondPhaseItem> &p_secondPhaseItems)
+{
+    if (!p_node) {
+        return true;
+    }
+
+    Q_ASSERT(testTarget(SearchTarget::SearchFile));
+
+    const auto name = p_node->getName();
+    if (!isFilePatternMatched(name)) {
+        return true;
+    }
+
+    const auto filePath = p_node->fetchAbsolutePath();
+    const auto relativePath = p_node->fetchPath();
+
+    if (testObject(SearchObject::SearchName)) {
+        if (isTokenMatched(name)) {
+            emit resultItemAdded(SearchResultItem::createFileItem(filePath, relativePath, -1, name));
+        }
+    }
+
+    if (testObject(SearchObject::SearchPath)) {
+        if (isTokenMatched(relativePath)) {
+            emit resultItemAdded(SearchResultItem::createFileItem(filePath, relativePath, -1, name));
+        }
+    }
+
+    if (testObject(SearchObject::SearchOutline)) {
+        emit logRequested(tr("Searching outline is not supported yet"));
+    }
+
+    if (testObject(SearchObject::SearchTag)) {
+        emit logRequested(tr("Searching tag is not supported yet"));
+    }
+
+    if (testObject(SearchObject::SearchContent)) {
+        p_secondPhaseItems.push_back(SearchSecondPhaseItem(filePath, relativePath));
+    }
+
+    return true;
+}
+
+bool Searcher::firstPhaseSearch(Notebook *p_notebook, QVector<SearchSecondPhaseItem> &p_secondPhaseItems)
+{
+    if (!p_notebook) {
+        return true;
+    }
+
+    if (testTarget(SearchTarget::SearchNotebook)) {
+        if (testObject(SearchObject::SearchName)) {
+            const auto name = p_notebook->getName();
+            if (isTokenMatched(name)) {
+                emit resultItemAdded(SearchResultItem::createNotebookItem(p_notebook->getRootFolderAbsolutePath(),
+                                                                          name));
+            }
+        }
+    }
+
+    if (!testTarget(SearchTarget::SearchFile) && !testTarget(SearchTarget::SearchFolder)) {
+        return true;
+    }
+
+    auto rootNode = p_notebook->getRootNode();
+    Q_ASSERT(rootNode->isLoaded());
+    const auto &children = rootNode->getChildrenRef();
+    for (const auto &child : children) {
+        if (isAskedToStop()) {
+            return true;
+        }
+
+        if (child->hasContent() && testTarget(SearchTarget::SearchFile)) {
+            if (!firstPhaseSearch(child.data(), p_secondPhaseItems)) {
+                return false;
+            }
+        }
+
+        if (child->isContainer()) {
+            if (!firstPhaseSearchFolder(child.data(), p_secondPhaseItems)) {
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+bool Searcher::secondPhaseSearch(const QVector<SearchSecondPhaseItem> &p_secondPhaseItems)
+{
+    Q_ASSERT(!p_secondPhaseItems.isEmpty());
+    qDebug() << "secondPhaseSearch" << p_secondPhaseItems.size();
+
+    createSearchEngine();
+
+    m_engine->search(m_option, m_token, p_secondPhaseItems);
+    connect(m_engine.data(), &ISearchEngine::finished,
+            this, &Searcher::finished);
+    connect(m_engine.data(), &ISearchEngine::logRequested,
+            this, &Searcher::logRequested);
+    connect(m_engine.data(), &ISearchEngine::resultItemsAdded,
+            this, &Searcher::resultItemsAdded);
+    return true;
+}
+
+void Searcher::createSearchEngine()
+{
+    Q_ASSERT(m_option->m_engine == SearchEngine::Internal);
+
+    m_engine.reset(new FileSearchEngine());
+}

+ 93 - 0
src/search/searcher.h

@@ -0,0 +1,93 @@
+#ifndef SEARCHER_H
+#define SEARCHER_H
+
+#include <QObject>
+#include <QSharedPointer>
+#include <QScopedPointer>
+#include <QRegularExpression>
+
+#include "searchdata.h"
+#include "searchtoken.h"
+#include "isearchengine.h"
+
+namespace vnotex
+{
+    class Buffer;
+    class File;
+    struct SearchResultItem;
+    class Node;
+    class Notebook;
+
+    class Searcher : public QObject
+    {
+        Q_OBJECT
+    public:
+        explicit Searcher(QObject *p_parent = nullptr);
+
+        void clear();
+
+        void stop();
+
+        SearchState search(const QSharedPointer<SearchOption> &p_option, const QList<Buffer *> &p_buffers);
+
+        SearchState search(const QSharedPointer<SearchOption> &p_option, Node *p_folder);
+
+        SearchState search(const QSharedPointer<SearchOption> &p_option, const QVector<Notebook *> &p_notebooks);
+
+    signals:
+        void progressUpdated(int p_val, int p_maximum);
+
+        void logRequested(const QString &p_log);
+
+        void resultItemAdded(const QSharedPointer<SearchResultItem> &p_item);
+
+        void resultItemsAdded(const QVector<QSharedPointer<SearchResultItem>> &p_items);
+
+        void finished(SearchState p_state);
+
+    private:
+        bool isAskedToStop() const;
+
+        bool prepare(const QSharedPointer<SearchOption> &p_option);
+
+        // Return false if there is failure.
+        // Always search content at first phase.
+        bool firstPhaseSearch(const File *p_file);
+
+        // Return false if there is failure.
+        bool firstPhaseSearchFolder(Node *p_node, QVector<SearchSecondPhaseItem> &p_secondPhaseItems);
+
+        // Return false if there is failure.
+        bool firstPhaseSearch(Node *p_node, QVector<SearchSecondPhaseItem> &p_secondPhaseItems);
+
+        // Return false if there is failure.
+        bool firstPhaseSearch(Notebook *p_notebook, QVector<SearchSecondPhaseItem> &p_secondPhaseItems);
+
+        // Return false if there is failure.
+        bool secondPhaseSearch(const QVector<SearchSecondPhaseItem> &p_secondPhaseItems);
+
+        bool isFilePatternMatched(const QString &p_name) const;
+
+        bool testTarget(SearchTarget p_target) const;
+
+        bool testObject(SearchObject p_object) const;
+
+        bool isTokenMatched(const QString &p_text) const;
+
+        bool searchContent(const File *p_file);
+
+        void createSearchEngine();
+
+        QSharedPointer<SearchOption> m_option;
+
+        SearchToken m_token;
+
+        QRegularExpression m_filePattern;
+
+        bool m_askedToStop = false;
+
+        QScopedPointer<ISearchEngine> m_engine;
+    };
+}
+
+#endif // SEARCHER_H

+ 54 - 0
src/search/searchresultitem.cpp

@@ -0,0 +1,54 @@
+#include "searchresultitem.h"
+
+using namespace vnotex;
+
+QSharedPointer<SearchResultItem> SearchResultItem::createBufferItem(const QString &p_targetPath,
+                                                                    const QString &p_displayPath,
+                                                                    int p_lineNumber,
+                                                                    const QString &p_text)
+{
+    auto item = QSharedPointer<SearchResultItem>::create();
+    item->m_location.m_type = LocationType::Buffer;
+    item->m_location.m_path = p_targetPath;
+    item->m_location.m_displayPath = p_displayPath;
+    item->m_location.addLine(p_lineNumber, p_text);
+    return item;
+}
+
+QSharedPointer<SearchResultItem> SearchResultItem::createFileItem(const QString &p_targetPath,
+                                                                  const QString &p_displayPath,
+                                                                  int p_lineNumber,
+                                                                  const QString &p_text)
+{
+    auto item = QSharedPointer<SearchResultItem>::create();
+    item->m_location.m_type = LocationType::File;
+    item->m_location.m_path = p_targetPath;
+    item->m_location.m_displayPath = p_displayPath;
+    item->m_location.addLine(p_lineNumber, p_text);
+    return item;
+}
+
+QSharedPointer<SearchResultItem> SearchResultItem::createFolderItem(const QString &p_targetPath,
+                                                                    const QString &p_displayPath)
+{
+    auto item = QSharedPointer<SearchResultItem>::create();
+    item->m_location.m_type = LocationType::Folder;
+    item->m_location.m_path = p_targetPath;
+    item->m_location.m_displayPath = p_displayPath;
+    return item;
+}
+
+QSharedPointer<SearchResultItem> SearchResultItem::createNotebookItem(const QString &p_targetPath,
+                                                                      const QString &p_displayPath)
+{
+    auto item = QSharedPointer<SearchResultItem>::create();
+    item->m_location.m_type = LocationType::Notebook;
+    item->m_location.m_path = p_targetPath;
+    item->m_location.m_displayPath = p_displayPath;
+    return item;
+}
+
+void SearchResultItem::addLine(int p_lineNumber, const QString &p_text)
+{
+    m_location.addLine(p_lineNumber, p_text);
+}

+ 42 - 0
src/search/searchresultitem.h

@@ -0,0 +1,42 @@
+#ifndef SEARCHRESULTITEM_H
+#define SEARCHRESULTITEM_H
+
+#include <QSharedPointer>
+#include <QString>
+#include <QDebug>
+
+#include <core/location.h>
+
+namespace vnotex
+{
+    struct SearchResultItem
+    {
+        friend QDebug operator<<(QDebug p_dbg, const SearchResultItem &p_item)
+        {
+            p_dbg << p_item.m_location;
+            return p_dbg;
+        }
+
+        void addLine(int p_lineNumber, const QString &p_text);
+
+        static QSharedPointer<SearchResultItem> createBufferItem(const QString &p_targetPath,
+                                                                 const QString &p_displayPath,
+                                                                 int p_lineNumber,
+                                                                 const QString &p_text);
+
+        static QSharedPointer<SearchResultItem> createFileItem(const QString &p_targetPath,
+                                                               const QString &p_displayPath,
+                                                               int p_lineNumber,
+                                                               const QString &p_text);
+
+        static QSharedPointer<SearchResultItem> createFolderItem(const QString &p_targetPath,
+                                                                 const QString &p_displayPath);
+
+        static QSharedPointer<SearchResultItem> createNotebookItem(const QString &p_targetPath,
+                                                                   const QString &p_displayPath);
+
+        ComplexLocation m_location;
+    };
+}
+
+#endif // SEARCHRESULTITEM_H

+ 247 - 0
src/search/searchtoken.cpp

@@ -0,0 +1,247 @@
+#include "searchtoken.h"
+
+#include <QCommandLineParser>
+#include <QDebug>
+
+#include <utils/processutils.h>
+#include <widgets/searchpanel.h>
+
+using namespace vnotex;
+
+QScopedPointer<QCommandLineParser> SearchToken::s_parser;
+
+void SearchToken::clear()
+{
+    m_type = Type::PlainText;
+    m_operator = Operator::And;
+    m_caseSensitivity = Qt::CaseInsensitive;
+    m_keywords.clear();
+    m_regularExpressions.clear();
+    m_matchedConstraintsInBatchMode.clear();
+    m_matchedConstraintsCountInBatchMode = 0;
+}
+
+void SearchToken::append(const QString &p_text)
+{
+    m_keywords.append(p_text);
+}
+
+void SearchToken::append(const QRegularExpression &p_regExp)
+{
+    m_regularExpressions.append(p_regExp);
+}
+
+bool SearchToken::matched(const QString &p_text) const
+{
+    const int consSize = constraintSize();
+    if (consSize == 0) {
+        return false;
+    }
+
+    bool isMatched = m_operator == Operator::And ? true : false;
+    for (int i = 0; i < consSize; ++i) {
+        bool consMatched = false;
+        if (m_type == Type::PlainText) {
+            consMatched = p_text.contains(m_keywords[i], m_caseSensitivity);
+        } else {
+            consMatched = p_text.contains(m_regularExpressions[i]);
+        }
+
+        if (consMatched) {
+            if (m_operator == Operator::Or) {
+                isMatched = true;
+                break;
+            }
+        } else if (m_operator == Operator::And) {
+            isMatched = false;
+            break;
+        }
+    }
+
+    return isMatched;
+}
+
+int SearchToken::constraintSize() const
+{
+    return (m_type == Type::PlainText ? m_keywords.size() : m_regularExpressions.size());
+}
+
+bool SearchToken::shouldStartBatchMode() const
+{
+    return constraintSize() > 1;
+}
+
+void SearchToken::startBatchMode()
+{
+    m_matchedConstraintsInBatchMode.fill(false, constraintSize());
+    m_matchedConstraintsCountInBatchMode = 0;
+}
+
+bool SearchToken::matchedInBatchMode(const QString &p_text)
+{
+    bool isMatched = false;
+    const int consSize = m_matchedConstraintsInBatchMode.size();
+    for (int i = 0; i < consSize; ++i) {
+        if (m_matchedConstraintsInBatchMode[i]) {
+            continue;
+        }
+
+        bool consMatched = false;
+        if (m_type == Type::PlainText) {
+            consMatched = p_text.contains(m_keywords[i], m_caseSensitivity);
+        } else {
+            consMatched = p_text.contains(m_regularExpressions[i]);
+        }
+
+        if (consMatched) {
+            m_matchedConstraintsInBatchMode[i] = true;
+            ++m_matchedConstraintsCountInBatchMode;
+            isMatched = true;
+        }
+    }
+
+    return isMatched;
+}
+
+bool SearchToken::readyToEndBatchMode() const
+{
+    if (m_operator == Operator::And) {
+        // We need all the tokens matched.
+        if (m_matchedConstraintsCountInBatchMode == m_matchedConstraintsInBatchMode.size()) {
+            return true;
+        }
+    } else {
+        // We only need one match.
+        if (m_matchedConstraintsCountInBatchMode > 0) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+void SearchToken::endBatchMode()
+{
+    m_matchedConstraintsInBatchMode.clear();
+    m_matchedConstraintsCountInBatchMode = 0;
+}
+
+bool SearchToken::isEmpty() const
+{
+    return constraintSize() == 0;
+}
+
+void SearchToken::createCommandLineParser()
+{
+    if (s_parser) {
+        return;
+    }
+
+    s_parser.reset(new QCommandLineParser());
+    s_parser->setApplicationDescription(SearchPanel::tr("Full-text search."));
+
+    QCommandLineOption caseSensitiveOpt(QStringList() << "c" << "case-sensitive", SearchPanel::tr("Search in case sensitive."));
+    s_parser->addOption(caseSensitiveOpt);
+
+    QCommandLineOption regularExpressionOpt(QStringList() << "r" << "regular-expression", SearchPanel::tr("Search by regular expression."));
+    s_parser->addOption(regularExpressionOpt);
+
+    QCommandLineOption wholeWordOnlyOpt(QStringList() << "w" << "whole-word-only", SearchPanel::tr("Search whole word only."));
+    s_parser->addOption(wholeWordOnlyOpt);
+
+    QCommandLineOption fuzzySearchOpt(QStringList() << "f" << "fuzzy-search", SearchPanel::tr("Do a fuzzy search (not applicable to content search)."));
+    s_parser->addOption(fuzzySearchOpt);
+
+    QCommandLineOption orOpt(QStringList() << "o" << "or", SearchPanel::tr("Do an OR combination of keywords."));
+    s_parser->addOption(orOpt);
+
+    s_parser->addPositionalArgument("keywords", SearchPanel::tr("Keywords to search."));
+}
+
+bool SearchToken::compile(const QString &p_keyword, FindOptions p_options, SearchToken &p_token)
+{
+
+    p_token.clear();
+
+    if (p_keyword.isEmpty()) {
+        return false;
+    }
+
+    createCommandLineParser();
+
+    auto caseSensitivity = p_options & FindOption::CaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive;
+    bool isRegularExpression = p_options & FindOption::RegularExpression;
+    bool isWholeWordOnly = p_options & FindOption::WholeWordOnly;
+    bool isFuzzySearch = p_options & FindOption::FuzzySearch;
+
+    auto args = ProcessUtils::parseCombinedArgString(p_keyword);
+    // The parser needs the first arg to be the application name.
+    args.prepend("vnotex");
+    if (!s_parser->parse(args))
+    {
+        return false;
+    }
+
+    if (s_parser->isSet("c")) {
+        caseSensitivity = Qt::CaseSensitive;
+    }
+    if (s_parser->isSet("r")) {
+        isRegularExpression = true;
+    }
+    if (s_parser->isSet("w")) {
+        isWholeWordOnly = true;
+    }
+    if (s_parser->isSet("f")) {
+        isFuzzySearch = true;
+    }
+
+    args = s_parser->positionalArguments();
+    if (args.isEmpty()) {
+        return false;
+    }
+
+    p_token.m_caseSensitivity = caseSensitivity;
+    if (isRegularExpression || isWholeWordOnly || isFuzzySearch) {
+        p_token.m_type = Type::RegularExpression;
+    } else {
+        p_token.m_type = Type::PlainText;
+    }
+    p_token.m_operator = s_parser->isSet("o") ? Operator::Or : Operator::And;
+
+    auto patternOptions = caseSensitivity == Qt::CaseInsensitive ? QRegularExpression::CaseInsensitiveOption
+                                                                 : QRegularExpression::NoPatternOption;
+    for (const auto &ar : args) {
+        if (ar.isEmpty()) {
+            continue;
+        }
+
+        if (isRegularExpression) {
+            p_token.append(QRegularExpression(ar, patternOptions));
+        } else if (isFuzzySearch) {
+            // ABC -> *A*B*C*.
+            QString wildcardText(ar.size() * 2 + 1, '*');
+            for (int i = 0, j = 1; i < ar.size(); ++i, j += 2) {
+                wildcardText[j] = ar[i];
+            }
+
+            p_token.append(QRegularExpression(QRegularExpression::wildcardToRegularExpression(wildcardText),
+                                              patternOptions));
+        } else if (isWholeWordOnly) {
+            auto pattern = QRegularExpression::escape(ar);
+            pattern = "\\b" + pattern + "\\b";
+            p_token.append(QRegularExpression(pattern, patternOptions));
+        } else {
+            p_token.append(ar);
+        }
+    }
+
+    return !p_token.isEmpty();
+}
+
+QString SearchToken::getHelpText()
+{
+    createCommandLineParser();
+    auto text = s_parser->helpText();
+    // Skip the first line containing the application name.
+    return text.mid(text.indexOf('\n') + 1);
+}

+ 86 - 0
src/search/searchtoken.h

@@ -0,0 +1,86 @@
+#ifndef SEARCHTOKEN_H
+#define SEARCHTOKEN_H
+
+#include <QString>
+#include <QVector>
+#include <QRegularExpression>
+#include <QBitArray>
+#include <QPair>
+#include <QScopedPointer>
+
+#include <core/global.h>
+
+class QCommandLineParser;
+
+namespace vnotex
+{
+    class SearchToken
+    {
+    public:
+        enum class Type
+        {
+            PlainText,
+            RegularExpression
+        };
+
+        enum class Operator
+        {
+            And,
+            Or
+        };
+
+        void clear();
+
+        void append(const QString &p_text);
+
+        void append(const QRegularExpression &p_regExp);
+
+        // Whether @p_text is matched.
+        bool matched(const QString &p_text) const;
+
+        int constraintSize() const;
+
+        bool isEmpty() const;
+
+        bool shouldStartBatchMode() const;
+
+        // Batch Mode: use a list of text string to match the same token.
+        void startBatchMode();
+
+        // Match one string in batch mode.
+        // Return true if @p_text is matched.
+        bool matchedInBatchMode(const QString &p_text);
+
+        bool readyToEndBatchMode() const;
+
+        void endBatchMode();
+
+        // Compile tokens from keyword.
+        // Support some magic switchs in the keyword which will suppress the given options.
+        static bool compile(const QString &p_keyword, FindOptions p_options, SearchToken &p_token);
+
+        static QString getHelpText();
+
+    private:
+        static void createCommandLineParser();
+
+        Type m_type = Type::PlainText;
+
+        Operator m_operator = Operator::And;
+
+        Qt::CaseSensitivity m_caseSensitivity = Qt::CaseInsensitive;
+
+        QStringList m_keywords;
+
+        QVector<QRegularExpression> m_regularExpressions;
+
+        // [i] is true only if m_keywords[i] or m_regularExpressions[i] is matched.
+        QBitArray m_matchedConstraintsInBatchMode;
+
+        int m_matchedConstraintsCountInBatchMode = 0;
+
+        static QScopedPointer<QCommandLineParser> s_parser;
+    };
+}
+
+#endif // SEARCHTOKEN_H

+ 2 - 0
src/src.pro

@@ -45,6 +45,8 @@ include($$PWD/utils/utils.pri)
 
 include($$PWD/export/export.pri)
 
+include($$PWD/search/search.pri)
+
 include($$PWD/core/core.pri)
 
 include($$PWD/widgets/widgets.pri)

+ 8 - 0
src/utils/widgetutils.cpp

@@ -22,6 +22,9 @@
 #include <QMenu>
 #include <QDebug>
 #include <QLineEdit>
+#include <QLayout>
+
+#include <core/global.h>
 
 using namespace vnotex;
 
@@ -361,3 +364,8 @@ void WidgetUtils::selectBaseName(QLineEdit *p_lineEdit)
     int dotIndex = text.lastIndexOf(QLatin1Char('.'));
     p_lineEdit->setSelection(0, (dotIndex == -1) ? text.size() : dotIndex);
 }
+
+void WidgetUtils::setContentsMargins(QLayout *p_layout)
+{
+    p_layout->setContentsMargins(CONTENTS_MARGIN, CONTENTS_MARGIN, CONTENTS_MARGIN, CONTENTS_MARGIN);
+}

+ 3 - 0
src/utils/widgetutils.h

@@ -18,6 +18,7 @@ class QListView;
 class QMenu;
 class QShortcut;
 class QLineEdit;
+class QLayout;
 
 namespace vnotex
 {
@@ -81,6 +82,8 @@ namespace vnotex
         // Select the base name part of the line edit content.
         static void selectBaseName(QLineEdit *p_lineEdit);
 
+        static void setContentsMargins(QLayout *p_layout);
+
     private:
         static void resizeToHideScrollBar(QScrollArea *p_scroll, bool p_vertical, bool p_horizontal);
     };

+ 1 - 1
src/widgets/attachmentpopup.cpp

@@ -204,7 +204,7 @@ void AttachmentPopup::setupUI()
 QToolButton *AttachmentPopup::createButton()
 {
     auto btn = new QToolButton(this);
-    btn->setProperty(PropertyDefs::s_actionToolButton, true);
+    btn->setProperty(PropertyDefs::c_actionToolButton, true);
     return btn;
 }
 

+ 2 - 2
src/widgets/dialogs/dialog.cpp

@@ -26,7 +26,7 @@ void Dialog::setCentralWidget(QWidget *p_widget)
 {
     Q_ASSERT(!m_centralWidget && p_widget);
     m_centralWidget = p_widget;
-    m_centralWidget->setProperty(PropertyDefs::s_dialogCentralWidget, true);
+    m_centralWidget->setProperty(PropertyDefs::c_dialogCentralWidget, true);
     m_layout->addWidget(m_centralWidget);
 }
 
@@ -115,7 +115,7 @@ void Dialog::setInformationText(const QString &p_text, InformationLevel p_level)
         break;
     }
 
-    WidgetUtils::setPropertyDynamically(m_infoTextEdit, PropertyDefs::s_state, level);
+    WidgetUtils::setPropertyDynamically(m_infoTextEdit, PropertyDefs::c_state, level);
     if (needResize) {
         WidgetUtils::updateSize(this);
     }

+ 2 - 2
src/widgets/dialogs/exportdialog.cpp

@@ -387,8 +387,8 @@ void ExportDialog::rejectedButtonClicked()
 {
     if (m_exportOngoing) {
         // Just cancel the export.
-        appendLog(tr("Cancelling the export."));
-        m_exporter->stop();
+        appendLog(tr("Cancelling the export"));
+        getExporter()->stop();
     } else {
         Dialog::rejectedButtonClicked();
     }

+ 1 - 1
src/widgets/dialogs/managenotebooksdialog.cpp

@@ -71,7 +71,7 @@ void ManageNotebooksDialog::setupUI()
                 });
 
         m_deleteNotebookBtn = new QPushButton(tr("Delete (DANGER)"), infoWidget);
-        WidgetUtils::setPropertyDynamically(m_deleteNotebookBtn, PropertyDefs::s_dangerButton, true);
+        WidgetUtils::setPropertyDynamically(m_deleteNotebookBtn, PropertyDefs::c_dangerButton, true);
         btnLayout->addWidget(m_deleteNotebookBtn);
         connect(m_deleteNotebookBtn, &QPushButton::clicked,
                 this, [this]() {

+ 1 - 1
src/widgets/dialogs/scrolldialog.cpp

@@ -29,7 +29,7 @@ void ScrollDialog::setCentralWidget(QWidget *p_widget)
 {
     Q_ASSERT(!m_centralWidget && p_widget);
     m_centralWidget = p_widget;
-    m_centralWidget->setProperty(PropertyDefs::s_dialogCentralWidget, true);
+    m_centralWidget->setProperty(PropertyDefs::c_dialogCentralWidget, true);
     m_scrollArea->setWidget(p_widget);
 }
 

+ 2 - 2
src/widgets/findandreplacewidget.cpp

@@ -55,7 +55,7 @@ void FindAndReplaceWidget::setupUI()
         const auto &themeMgr = VNoteX::getInst().getThemeMgr();
         auto iconFile = themeMgr.getIconFile(QStringLiteral("close.svg"));
         auto closeBtn = new QToolButton(this);
-        closeBtn->setProperty(PropertyDefs::s_actionToolButton, true);
+        closeBtn->setProperty(PropertyDefs::c_actionToolButton, true);
         titleLayout->addWidget(closeBtn);
 
         auto closeAct = new QAction(IconUtils::fetchIcon(iconFile), QString(), closeBtn);
@@ -223,7 +223,7 @@ void FindAndReplaceWidget::updateFindOptions()
         return;
     }
 
-    FindOptions options = FindOption::None;
+    FindOptions options = FindOption::FindNone;
 
     if (m_caseSensitiveCheckBox->isChecked()) {
         options |= FindOption::CaseSensitive;

+ 1 - 1
src/widgets/findandreplacewidget.h

@@ -77,7 +77,7 @@ namespace vnotex
 
         QCheckBox *m_incrementalSearchCheckBox = nullptr;
 
-        FindOptions m_options = FindOption::None;
+        FindOptions m_options = FindOption::FindNone;
 
         QTimer *m_findTextTimer = nullptr;
 

+ 180 - 0
src/widgets/locationlist.cpp

@@ -0,0 +1,180 @@
+#include "locationlist.h"
+
+#include <QVBoxLayout>
+#include <QToolButton>
+
+#include "treewidget.h"
+#include "widgetsfactory.h"
+#include "titlebar.h"
+
+#include <core/vnotex.h>
+#include <utils/iconutils.h>
+
+using namespace vnotex;
+
+QIcon LocationList::s_bufferIcon;
+
+QIcon LocationList::s_fileIcon;
+
+QIcon LocationList::s_folderIcon;
+
+QIcon LocationList::s_notebookIcon;
+
+LocationList::LocationList(QWidget *p_parent)
+    : QFrame(p_parent)
+{
+    setupUI();
+}
+
+void LocationList::setupUI()
+{
+    auto mainLayout = new QVBoxLayout(this);
+    mainLayout->setContentsMargins(0, 0, 0, 0);
+    mainLayout->setSpacing(0);
+
+    {
+        setupTitleBar(QString(), this);
+        mainLayout->addWidget(m_titleBar);
+    }
+
+    m_tree = new TreeWidget(TreeWidget::Flag::None, this);
+    // When updated, pay attention to the Columns enum.
+    m_tree->setHeaderLabels(QStringList() << tr("Path") << tr("Line") << tr("Text"));
+    TreeWidget::showHorizontalScrollbar(m_tree);
+    connect(m_tree, &QTreeWidget::itemActivated,
+            this, [this](QTreeWidgetItem *p_item, int p_col) {
+                Q_UNUSED(p_col);
+                if (!m_callback) {
+                    return;
+                }
+                m_callback(getItemLocation(p_item));
+            });
+    mainLayout->addWidget(m_tree);
+
+    setFocusProxy(m_tree);
+}
+
+const QIcon &LocationList::getItemIcon(LocationType p_type)
+{
+    if (s_bufferIcon.isNull()) {
+        // Init.
+        const QString nodeIconFgName = "widgets#locationlist#node_icon#fg";
+        const auto &themeMgr = VNoteX::getInst().getThemeMgr();
+        const auto fg = themeMgr.paletteColor(nodeIconFgName);
+
+        s_bufferIcon = IconUtils::fetchIcon(themeMgr.getIconFile("buffer.svg"), fg);
+        s_fileIcon = IconUtils::fetchIcon(themeMgr.getIconFile("file_node.svg"), fg);
+        s_folderIcon = IconUtils::fetchIcon(themeMgr.getIconFile("folder_node.svg"), fg);
+        s_notebookIcon = IconUtils::fetchIcon(themeMgr.getIconFile("notebook_default.svg"), fg);
+    }
+
+    switch (p_type) {
+    case LocationType::Buffer:
+        return s_bufferIcon;
+
+    case LocationType::File:
+        return s_fileIcon;
+
+    case LocationType::Folder:
+        return s_folderIcon;
+
+    case LocationType::Notebook:
+        Q_FALLTHROUGH();
+    default:
+        return s_notebookIcon;
+    }
+}
+
+NavigationModeWrapper<QTreeWidget, QTreeWidgetItem> *LocationList::getNavigationModeWrapper()
+{
+    if (!m_navigationWrapper) {
+        m_navigationWrapper.reset(new NavigationModeWrapper<QTreeWidget, QTreeWidgetItem>(m_tree));
+    }
+    return m_navigationWrapper.data();
+}
+
+void LocationList::setupTitleBar(const QString &p_title, QWidget *p_parent)
+{
+    m_titleBar = new TitleBar(p_title, true, TitleBar::Action::None, p_parent);
+
+    {
+        auto clearBtn = m_titleBar->addActionButton(QStringLiteral("clear.svg"), tr("Clear"));
+        connect(clearBtn, &QToolButton::triggered,
+                this, &LocationList::clear);
+    }
+}
+
+void LocationList::clear()
+{
+    m_tree->clear();
+
+    m_callback = LocationCallback();
+
+    updateItemsCountLabel();
+}
+
+void LocationList::setItemLocationLineAndText(QTreeWidgetItem *p_item, const ComplexLocation::Line &p_line)
+{
+    p_item->setData(Columns::LineColumn, Qt::UserRole, p_line.m_lineNumber);
+    if (p_line.m_lineNumber != -1) {
+        p_item->setText(Columns::LineColumn, QString::number(p_line.m_lineNumber));
+    }
+    p_item->setText(Columns::TextColumn, p_line.m_text);
+}
+
+void LocationList::addLocation(const ComplexLocation &p_location)
+{
+    auto item = new QTreeWidgetItem(m_tree);
+    item->setText(Columns::PathColumn, p_location.m_displayPath);
+    item->setData(Columns::PathColumn, Qt::UserRole, p_location.m_path);
+
+    item->setIcon(Columns::PathColumn, getItemIcon(p_location.m_type));
+
+    if (p_location.m_lines.size() == 1) {
+        setItemLocationLineAndText(item, p_location.m_lines[0]);
+    } else if (p_location.m_lines.size() > 1) {
+        // Add sub items.
+        for (const auto &line : p_location.m_lines) {
+            auto subItem = new QTreeWidgetItem(item);
+            setItemLocationLineAndText(subItem, line);
+        }
+
+        item->setExpanded(true);
+    }
+
+    updateItemsCountLabel();
+}
+
+void LocationList::startSession(const LocationCallback &p_callback)
+{
+    m_callback = p_callback;
+}
+
+Location LocationList::getItemLocation(const QTreeWidgetItem *p_item) const
+{
+    Location loc;
+
+    if (!p_item) {
+        return loc;
+    }
+
+    auto paItem = p_item->parent() ? p_item->parent() : p_item;
+    loc.m_path = paItem->data(Columns::PathColumn, Qt::UserRole).toString();
+    loc.m_displayPath = paItem->text(Columns::PathColumn);
+
+    auto lineNumberData = p_item->data(Columns::LineColumn, Qt::UserRole);
+    if (lineNumberData.isValid()) {
+        loc.m_lineNumber = lineNumberData.toInt();
+    }
+    return loc;
+}
+
+void LocationList::updateItemsCountLabel()
+{
+    const auto cnt = m_tree->topLevelItemCount();
+    if (cnt == 0) {
+        m_titleBar->setInfoLabel("");
+    } else {
+        m_titleBar->setInfoLabel(tr("%n Item(s)", "", m_tree->topLevelItemCount()));
+    }
+}

+ 74 - 0
src/widgets/locationlist.h

@@ -0,0 +1,74 @@
+#ifndef LOCATIONLIST_H
+#define LOCATIONLIST_H
+
+#include <functional>
+
+#include <QFrame>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <QIcon>
+
+#include <core/location.h>
+
+#include "navigationmodewrapper.h"
+
+namespace vnotex
+{
+    class TitleBar;
+
+    class LocationList : public QFrame
+    {
+        Q_OBJECT
+    public:
+        typedef std::function<void(const Location &)> LocationCallback;
+
+        explicit LocationList(QWidget *p_parent = nullptr);
+
+        NavigationModeWrapper<QTreeWidget, QTreeWidgetItem> *getNavigationModeWrapper();
+
+        void clear();
+
+        void addLocation(const ComplexLocation &p_location);
+
+        // Start a new session of the location list to set a callback for activation handling.
+        void startSession(const LocationCallback &p_callback);
+
+    private:
+        enum Columns
+        {
+            PathColumn = 0,
+            LineColumn,
+            TextColumn
+        };
+
+        void setupUI();
+
+        void setupTitleBar(const QString &p_title, QWidget *p_parent = nullptr);
+
+        void setItemLocationLineAndText(QTreeWidgetItem *p_item, const ComplexLocation::Line &p_line);
+
+        const QIcon &getItemIcon(LocationType p_type);
+
+        Location getItemLocation(const QTreeWidgetItem *p_item) const;
+
+        void updateItemsCountLabel();
+
+        TitleBar *m_titleBar = nullptr;
+
+        QTreeWidget *m_tree = nullptr;
+
+        QScopedPointer<NavigationModeWrapper<QTreeWidget, QTreeWidgetItem>> m_navigationWrapper;
+
+        LocationCallback m_callback;
+
+        static QIcon s_bufferIcon;
+
+        static QIcon s_fileIcon;
+
+        static QIcon s_folderIcon;
+
+        static QIcon s_notebookIcon;
+    };
+}
+
+#endif // LOCATIONLIST_H

+ 93 - 29
src/widgets/mainwindow.cpp

@@ -38,6 +38,10 @@
 #include "messageboxhelper.h"
 #include "systemtrayhelper.h"
 #include "titletoolbar.h"
+#include "locationlist.h"
+#include "searchpanel.h"
+#include <notebook/notebook.h>
+#include "searchinfoprovider.h"
 
 using namespace vnotex;
 
@@ -193,10 +197,14 @@ void MainWindow::setupDocks()
 
     setupOutlineDock();
 
+    setupSearchDock();
+
     for (int i = 1; i < m_docks.size(); ++i) {
         tabifyDockWidget(m_docks[i - 1], m_docks[i]);
     }
 
+    setupLocationListDock();
+
     for (auto dock : m_docks) {
         connect(dock, &QDockWidget::visibilityChanged,
                 this, [this]() {
@@ -205,8 +213,7 @@ void MainWindow::setupDocks()
                 });
     }
 
-    // Activate the first dock.
-    activateDock(m_docks[0]);
+    activateDock(m_docks[DockIndex::NavigationDock]);
 }
 
 void MainWindow::activateDock(QDockWidget *p_dock)
@@ -257,6 +264,53 @@ void MainWindow::setupOutlineDock()
     addDockWidget(Qt::LeftDockWidgetArea, dock);
 }
 
+void MainWindow::setupSearchDock()
+{
+    auto dock = new QDockWidget(tr("Search"), this);
+    m_docks.push_back(dock);
+
+    dock->setObjectName(QStringLiteral("SearchDock.vnotex"));
+    dock->setAllowedAreas(Qt::AllDockWidgetAreas);
+
+    setupSearchPanel();
+    dock->setWidget(m_searchPanel);
+    dock->setFocusProxy(m_searchPanel);
+    addDockWidget(Qt::LeftDockWidgetArea, dock);
+}
+
+void MainWindow::setupSearchPanel()
+{
+    m_searchPanel = new SearchPanel(
+        QSharedPointer<SearchInfoProvider>::create(m_viewArea,
+                                                   m_notebookExplorer,
+                                                   &VNoteX::getInst().getNotebookMgr()),
+        this);
+    m_searchPanel->setObjectName("SearchPanel.vnotex");
+}
+
+void MainWindow::setupLocationListDock()
+{
+    auto dock = new QDockWidget(tr("Location List"), this);
+    m_docks.push_back(dock);
+
+    dock->setObjectName(QStringLiteral("LocationListDock.vnotex"));
+    dock->setAllowedAreas(Qt::AllDockWidgetAreas);
+
+    setupLocationList();
+    dock->setWidget(m_locationList);
+    dock->setFocusProxy(m_locationList);
+    addDockWidget(Qt::BottomDockWidgetArea, dock);
+    dock->hide();
+}
+
+void MainWindow::setupLocationList()
+{
+    m_locationList = new LocationList(this);
+    m_locationList->setObjectName("LocationList.vnotex");
+
+    NavigationModeMgr::getInst().registerNavigationTarget(m_locationList->getNavigationModeWrapper());
+}
+
 void MainWindow::setupNavigationToolBox()
 {
     m_navigationToolBox = new ToolBox(this);
@@ -522,34 +576,30 @@ void MainWindow::closeOnQuit()
 void MainWindow::setupShortcuts()
 {
     const auto &coreConfig = ConfigMgr::getInst().getCoreConfig();
-    // Focus Navigation dock.
-    {
-        auto keys = coreConfig.getShortcut(CoreConfig::Shortcut::NavigationDock);
-        auto shortcut = WidgetUtils::createShortcut(keys, this);
-        if (shortcut) {
-            auto dock = m_docks[DockIndex::NavigationDock];
-            dock->setToolTip(QString("%1\t%2").arg(dock->windowTitle(),
-                                                   QKeySequence(keys).toString(QKeySequence::NativeText)));
-            connect(shortcut, &QShortcut::activated,
-                    this, [this]() {
-                        activateDock(m_docks[DockIndex::NavigationDock]);
-                    });
-        }
-    }
 
-    // Focus Outline dock.
-    {
-        auto keys = coreConfig.getShortcut(CoreConfig::Shortcut::OutlineDock);
-        auto shortcut = WidgetUtils::createShortcut(keys, this);
-        if (shortcut) {
-            auto dock = m_docks[DockIndex::OutlineDock];
-            dock->setToolTip(QString("%1\t%2").arg(dock->windowTitle(),
-                                                   QKeySequence(keys).toString(QKeySequence::NativeText)));
-            connect(shortcut, &QShortcut::activated,
-                    this, [this]() {
-                        activateDock(m_docks[DockIndex::OutlineDock]);
-                    });
-        }
+    setupDockActivateShortcut(m_docks[DockIndex::NavigationDock],
+                              coreConfig.getShortcut(CoreConfig::Shortcut::NavigationDock));
+
+    setupDockActivateShortcut(m_docks[DockIndex::OutlineDock],
+                              coreConfig.getShortcut(CoreConfig::Shortcut::OutlineDock));
+
+    setupDockActivateShortcut(m_docks[DockIndex::SearchDock],
+                              coreConfig.getShortcut(CoreConfig::Shortcut::SearchDock));
+
+    setupDockActivateShortcut(m_docks[DockIndex::LocationListDock],
+                              coreConfig.getShortcut(CoreConfig::Shortcut::LocationListDock));
+}
+
+void MainWindow::setupDockActivateShortcut(QDockWidget *p_dock, const QString &p_keys)
+{
+    auto shortcut = WidgetUtils::createShortcut(p_keys, this);
+    if (shortcut) {
+        p_dock->setToolTip(QString("%1\t%2").arg(p_dock->windowTitle(),
+                                                 QKeySequence(p_keys).toString(QKeySequence::NativeText)));
+        connect(shortcut, &QShortcut::activated,
+                this, [this, p_dock]() {
+                    activateDock(p_dock);
+                });
     }
 }
 
@@ -684,3 +734,17 @@ void MainWindow::setTipsAreaVisible(bool p_visible)
         m_tipsLabel->hide();
     }
 }
+
+LocationList *MainWindow::getLocationList() const
+{
+    return m_locationList;
+}
+
+void MainWindow::setLocationListVisible(bool p_visible)
+{
+    if (p_visible) {
+        activateDock(m_docks[DockIndex::LocationListDock]);
+    } else {
+        m_docks[DockIndex::LocationListDock]->hide();
+    }
+}

+ 23 - 1
src/widgets/mainwindow.h

@@ -19,6 +19,8 @@ namespace vnotex
     class ViewArea;
     class Event;
     class OutlineViewer;
+    class LocationList;
+    class SearchPanel;
 
     enum { RESTART_EXIT_CODE = 1000 };
 
@@ -55,6 +57,10 @@ namespace vnotex
 
         void openFiles(const QStringList &p_files);
 
+        LocationList *getLocationList() const;
+
+        void setLocationListVisible(bool p_visible);
+
     signals:
         void mainWindowStarted();
 
@@ -85,7 +91,9 @@ namespace vnotex
         enum DockIndex
         {
             NavigationDock = 0,
-            OutlineDock
+            OutlineDock,
+            SearchDock,
+            LocationListDock
         };
 
         void setupUI();
@@ -100,6 +108,14 @@ namespace vnotex
 
         void setupOutlineDock();
 
+        void setupSearchDock();
+
+        void setupSearchPanel();
+
+        void setupLocationListDock();
+
+        void setupLocationList();
+
         void setupNotebookExplorer(QWidget *p_parent = nullptr);
 
         void setupDocks();
@@ -129,6 +145,8 @@ namespace vnotex
 
         void setTipsAreaVisible(bool p_visible);
 
+        void setupDockActivateShortcut(QDockWidget *p_dock, const QString &p_keys);
+
         ToolBarHelper m_toolBarHelper;
 
         StatusBarHelper m_statusBarHelper;
@@ -143,6 +161,10 @@ namespace vnotex
 
         OutlineViewer *m_outlineViewer = nullptr;
 
+        LocationList *m_locationList = nullptr;
+
+        SearchPanel *m_searchPanel = nullptr;
+
         QVector<QDockWidget *> m_docks;
 
         bool m_layoutReset = false;

+ 5 - 5
src/widgets/markdownviewwindow.cpp

@@ -840,7 +840,9 @@ void MarkdownViewWindow::zoom(bool p_zoomIn)
 void MarkdownViewWindow::handleFindTextChanged(const QString &p_text, FindOptions p_options)
 {
     if (m_mode == Mode::Read) {
-        adapter()->findText(p_text, p_options);
+        if (p_options & FindOption::IncrementalSearch) {
+            adapter()->findText(p_text, p_options);
+        }
     } else {
         TextViewWindowHelper::handleFindTextChanged(this, p_text, p_options);
     }
@@ -849,9 +851,7 @@ void MarkdownViewWindow::handleFindTextChanged(const QString &p_text, FindOption
 void MarkdownViewWindow::handleFindNext(const QString &p_text, FindOptions p_options)
 {
     if (m_mode == Mode::Read) {
-        if (p_options & FindOption::IncrementalSearch) {
-            adapter()->findText(p_text, p_options);
-        }
+        adapter()->findText(p_text, p_options);
     } else {
         TextViewWindowHelper::handleFindNext(this, p_text, p_options);
     }
@@ -880,7 +880,7 @@ void MarkdownViewWindow::handleFindAndReplaceWidgetClosed()
     if (m_editor) {
         TextViewWindowHelper::handleFindAndReplaceWidgetClosed(this);
     } else {
-        adapter()->findText("", FindOption::None);
+        adapter()->findText("", FindOption::FindNone);
     }
 }
 

+ 2 - 1
src/widgets/notebookexplorer.cpp

@@ -45,7 +45,7 @@ NotebookExplorer::NotebookExplorer(QWidget *p_parent)
 void NotebookExplorer::setupUI()
 {
     auto mainLayout = new QVBoxLayout(this);
-    mainLayout->setContentsMargins(0, 0, 0, 0);
+    WidgetUtils::setContentsMargins(mainLayout);
 
     // Title bar.
     auto titleBar = setupTitleBar(this);
@@ -88,6 +88,7 @@ TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent)
     const auto &widgetConfig = ConfigMgr::getInst().getWidgetConfig();
 
     auto titleBar = new TitleBar(tr("Notebook"),
+                                 false,
                                  TitleBar::Action::Menu,
                                  p_parent);
     titleBar->setWhatsThis(tr("This title bar contains buttons and menu to manage notebooks and notes."));

+ 1 - 1
src/widgets/notebooknodeexplorer.cpp

@@ -941,7 +941,7 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
 
     case Action::Properties:
         act = new QAction(generateMenuActionIcon("properties.svg"),
-                          tr("&Properties"),
+                          tr("&Properties (Rename)"),
                           p_parent);
         connect(act, &QAction::triggered,
                 this, [this]() {

+ 3 - 0
src/widgets/outlinepopup.cpp

@@ -3,6 +3,8 @@
 #include <QVBoxLayout>
 #include <QToolButton>
 
+#include <core/global.h>
+#include <utils/widgetutils.h>
 #include "outlineviewer.h"
 
 using namespace vnotex;
@@ -22,6 +24,7 @@ OutlinePopup::OutlinePopup(QToolButton *p_btn, QWidget *p_parent)
 void OutlinePopup::setupUI()
 {
     auto mainLayout = new QVBoxLayout(this);
+    WidgetUtils::setContentsMargins(mainLayout);
 
     m_viewer = new OutlineViewer(tr("Outline"), this);
     mainLayout->addWidget(m_viewer);

+ 4 - 3
src/widgets/outlineviewer.cpp

@@ -9,6 +9,8 @@
 #include <QToolTip>
 #include <QDebug>
 
+#include <utils/widgetutils.h>
+
 #include "treewidget.h"
 #include "titlebar.h"
 
@@ -50,8 +52,7 @@ OutlineViewer::OutlineViewer(const QString &p_title, QWidget *p_parent)
 void OutlineViewer::setupUI(const QString &p_title)
 {
     auto mainLayout = new QVBoxLayout(this);
-    mainLayout->setContentsMargins(0, 0, 0, 0);
-    mainLayout->setSpacing(0);
+    WidgetUtils::setContentsMargins(mainLayout);
 
     {
         auto titleBar = setupTitleBar(p_title, this);
@@ -88,7 +89,7 @@ NavigationModeWrapper<QTreeWidget, QTreeWidgetItem> *OutlineViewer::getNavigatio
 
 TitleBar *OutlineViewer::setupTitleBar(const QString &p_title, QWidget *p_parent)
 {
-    auto titleBar = new TitleBar(p_title, TitleBar::Action::None, p_parent);
+    auto titleBar = new TitleBar(p_title, false, TitleBar::Action::None, p_parent);
 
     auto decreaseBtn = titleBar->addActionButton(QStringLiteral("decrease_outline_level.svg"), tr("Decrease Expansion Level"));
     connect(decreaseBtn, &QToolButton::clicked,

+ 1 - 1
src/widgets/outlineviewer.h

@@ -22,7 +22,7 @@ namespace vnotex
     {
         Q_OBJECT
     public:
-        explicit OutlineViewer(const QString &p_title, QWidget *p_parent = nullptr);
+        OutlineViewer(const QString &p_title, QWidget *p_parent = nullptr);
 
         void setOutlineProvider(const QSharedPointer<OutlineProvider> &p_provider);
 

+ 10 - 8
src/widgets/propertydefs.cpp

@@ -2,18 +2,20 @@
 
 using namespace vnotex;
 
-const char *PropertyDefs::s_actionToolButton = "ActionToolButton";
+const char *PropertyDefs::c_actionToolButton = "ActionToolButton";
 
-const char *PropertyDefs::s_toolButtonWithoutMenuIndicator = "NoMenuIndicator";
+const char *PropertyDefs::c_toolButtonWithoutMenuIndicator = "NoMenuIndicator";
 
-const char *PropertyDefs::s_dangerButton = "DangerButton";
+const char *PropertyDefs::c_dangerButton = "DangerButton";
 
-const char *PropertyDefs::s_dialogCentralWidget = "DialogCentralWidget";
+const char *PropertyDefs::c_dialogCentralWidget = "DialogCentralWidget";
 
-const char *PropertyDefs::s_viewSplitCornerWidget = "ViewSplitCornerWidget";
+const char *PropertyDefs::c_viewSplitCornerWidget = "ViewSplitCornerWidget";
 
-const char *PropertyDefs::s_state = "State";
+const char *PropertyDefs::c_state = "State";
 
-const char *PropertyDefs::s_viewWindowToolBar = "ViewWindowToolBar";
+const char *PropertyDefs::c_viewWindowToolBar = "ViewWindowToolBar";
 
-const char *PropertyDefs::s_consoleTextEdit = "ConsoleTextEdit";
+const char *PropertyDefs::c_consoleTextEdit = "ConsoleTextEdit";
+
+const char *PropertyDefs::c_embeddedLineEdit = "EmbeddedLineEdit";

+ 10 - 8
src/widgets/propertydefs.h

@@ -9,22 +9,24 @@ namespace vnotex
     public:
         PropertyDefs() = delete;
 
-        static const char *s_actionToolButton;
+        static const char *c_actionToolButton;
 
-        static const char *s_toolButtonWithoutMenuIndicator;
+        static const char *c_toolButtonWithoutMenuIndicator;
 
-        static const char *s_dangerButton;
+        static const char *c_dangerButton;
 
-        static const char *s_dialogCentralWidget;
+        static const char *c_dialogCentralWidget;
 
-        static const char *s_viewSplitCornerWidget;
+        static const char *c_viewSplitCornerWidget;
 
-        static const char *s_viewWindowToolBar;
+        static const char *c_viewWindowToolBar;
 
-        static const char *s_consoleTextEdit;
+        static const char *c_consoleTextEdit;
+
+        static const char *c_embeddedLineEdit;
 
         // Values: info/warning/error.
-        static const char *s_state;
+        static const char *c_state;
     };
 }
 

+ 43 - 0
src/widgets/searchinfoprovider.cpp

@@ -0,0 +1,43 @@
+#include "searchinfoprovider.h"
+
+#include "viewarea.h"
+#include "notebookexplorer.h"
+#include "notebookmgr.h"
+
+using namespace vnotex;
+
+SearchInfoProvider::SearchInfoProvider(const ViewArea *p_viewArea,
+                                       const NotebookExplorer *p_notebookExplorer,
+                                       const NotebookMgr *p_notebookMgr)
+    : m_viewArea(p_viewArea),
+      m_notebookExplorer(p_notebookExplorer),
+      m_notebookMgr(p_notebookMgr)
+{
+}
+
+QList<Buffer *> SearchInfoProvider::getBuffers() const
+{
+    return m_viewArea->getAllBuffersInViewSplits();
+}
+
+Node *SearchInfoProvider::getCurrentFolder() const
+{
+    return m_notebookExplorer->currentExploredFolderNode();
+}
+
+Notebook *SearchInfoProvider::getCurrentNotebook() const
+{
+    return m_notebookExplorer->currentNotebook().data();
+}
+
+QVector<Notebook *> SearchInfoProvider::getNotebooks() const
+{
+    auto notebooks = m_notebookMgr->getNotebooks();
+    QVector<Notebook *> nbs;
+    nbs.reserve(notebooks.size());
+    for (const auto &nb : notebooks) {
+        nbs.push_back(nb.data());
+    }
+
+    return nbs;
+}

+ 36 - 0
src/widgets/searchinfoprovider.h

@@ -0,0 +1,36 @@
+#ifndef SEARCHINFOPROVIDER_H
+#define SEARCHINFOPROVIDER_H
+
+#include "searchpanel.h"
+
+namespace vnotex
+{
+    class ViewArea;
+    class NotebookExplorer;
+    class NotebookMgr;
+
+    class SearchInfoProvider : public ISearchInfoProvider
+    {
+    public:
+        SearchInfoProvider(const ViewArea *p_viewArea,
+                           const NotebookExplorer *p_notebookExplorer,
+                           const NotebookMgr *p_notebookMgr);
+
+        QList<Buffer *> getBuffers() const Q_DECL_OVERRIDE;
+
+        Node *getCurrentFolder() const Q_DECL_OVERRIDE;
+
+        Notebook *getCurrentNotebook() const Q_DECL_OVERRIDE;
+
+        QVector<Notebook *> getNotebooks() const Q_DECL_OVERRIDE;
+
+    private:
+        const ViewArea *m_viewArea = nullptr;
+
+        const NotebookExplorer *m_notebookExplorer = nullptr;
+
+        const NotebookMgr *m_notebookMgr = nullptr;
+    };
+}
+
+#endif // SEARCHINFOPROVIDER_H

+ 526 - 0
src/widgets/searchpanel.cpp

@@ -0,0 +1,526 @@
+#include "searchpanel.h"
+
+#include <QVBoxLayout>
+#include <QToolButton>
+#include <QLabel>
+#include <QFormLayout>
+#include <QComboBox>
+#include <QCheckBox>
+#include <QLineEdit>
+#include <QCompleter>
+#include <QGridLayout>
+#include <QProgressBar>
+#include <QPlainTextEdit>
+#include <QCoreApplication>
+#include <QRadioButton>
+#include <QButtonGroup>
+
+#include <core/configmgr.h>
+#include <core/sessionconfig.h>
+#include <core/vnotex.h>
+#include <core/fileopenparameters.h>
+#include <notebook/node.h>
+#include <notebook/notebook.h>
+#include "widgetsfactory.h"
+#include "titlebar.h"
+#include "propertydefs.h"
+#include "mainwindow.h"
+#include <search/searchtoken.h>
+#include <search/searchresultitem.h>
+#include <utils/widgetutils.h>
+#include "locationlist.h"
+
+using namespace vnotex;
+
+SearchPanel::SearchPanel(const QSharedPointer<ISearchInfoProvider> &p_provider, QWidget *p_parent)
+    : QFrame(p_parent),
+      m_provider(p_provider)
+{
+    qRegisterMetaType<QVector<QSharedPointer<SearchResultItem>>>("QVector<QSharedPointer<SearchResultItem>>");
+
+    setupUI();
+
+    initOptions();
+
+    restoreFields(*m_option);
+}
+
+void SearchPanel::setupUI()
+{
+    auto mainLayout = new QVBoxLayout(this);
+    WidgetUtils::setContentsMargins(mainLayout);
+
+    {
+        auto titleBar = setupTitleBar(QString(), this);
+        mainLayout->addWidget(titleBar);
+    }
+
+    auto inputsLayout = new QFormLayout();
+    mainLayout->addLayout(inputsLayout);
+
+    m_keywordComboBox = WidgetsFactory::createComboBox(this);
+    m_keywordComboBox->setToolTip(SearchToken::getHelpText());
+    m_keywordComboBox->setEditable(true);
+    m_keywordComboBox->setLineEdit(WidgetsFactory::createLineEdit(this));
+    m_keywordComboBox->lineEdit()->setProperty(PropertyDefs::c_embeddedLineEdit, true);
+    m_keywordComboBox->completer()->setCaseSensitivity(Qt::CaseSensitive);
+    connect(m_keywordComboBox->lineEdit(), &QLineEdit::returnPressed,
+            this, [this]() {
+                m_searchBtn->animateClick();
+            });
+    inputsLayout->addRow(tr("Keyword:"), m_keywordComboBox);
+
+    m_searchScopeComboBox = WidgetsFactory::createComboBox(this);
+    m_searchScopeComboBox->addItem(tr("Buffers"), static_cast<int>(SearchScope::Buffers));
+    m_searchScopeComboBox->addItem(tr("Current Folder"), static_cast<int>(SearchScope::CurrentFolder));
+    m_searchScopeComboBox->addItem(tr("Current Notebook"), static_cast<int>(SearchScope::CurrentNotebook));
+    m_searchScopeComboBox->addItem(tr("All Notebooks"), static_cast<int>(SearchScope::AllNotebooks));
+    inputsLayout->addRow(tr("Scope:"), m_searchScopeComboBox);
+
+    setupSearchObject(inputsLayout);
+
+    setupSearchTarget(inputsLayout);
+
+    m_filePatternComboBox = WidgetsFactory::createComboBox(this);
+    m_filePatternComboBox->setEditable(true);
+    m_filePatternComboBox->setLineEdit(WidgetsFactory::createLineEdit(this));
+    m_filePatternComboBox->lineEdit()->setPlaceholderText(tr("Wildcard pattern of files and folders to search"));
+    m_filePatternComboBox->lineEdit()->setProperty(PropertyDefs::c_embeddedLineEdit, true);
+    m_filePatternComboBox->completer()->setCaseSensitivity(Qt::CaseSensitive);
+    inputsLayout->addRow(tr("File pattern:"), m_filePatternComboBox);
+
+    setupFindOption(inputsLayout);
+
+    {
+        m_progressBar = new QProgressBar(this);
+        m_progressBar->setRange(0, 0);
+        m_progressBar->hide();
+        mainLayout->addWidget(m_progressBar);
+    }
+
+    mainLayout->addStretch();
+}
+
+TitleBar *SearchPanel::setupTitleBar(const QString &p_title, QWidget *p_parent)
+{
+    auto titleBar = new TitleBar(p_title, false, TitleBar::Action::None, p_parent);
+    titleBar->setActionButtonsAlwaysShown(true);
+
+    {
+        m_searchBtn = titleBar->addActionButton(QStringLiteral("search.svg"), tr("Search"));
+        connect(m_searchBtn, &QToolButton::triggered,
+                this, &SearchPanel::startSearch);
+
+        auto cancelBtn = titleBar->addActionButton(QStringLiteral("cancel.svg"), tr("Cancel"));
+        connect(cancelBtn, &QToolButton::triggered,
+                this, &SearchPanel::stopSearch);
+
+        auto closeLocationListBtn = titleBar->addActionButton(QStringLiteral("close.svg"), tr("Close Location List"));
+        connect(closeLocationListBtn, &QToolButton::triggered,
+                this, [this]() {
+
+                });
+    }
+
+    return titleBar;
+}
+
+void SearchPanel::setupSearchObject(QFormLayout *p_layout)
+{
+    auto gridLayout = new QGridLayout();
+    p_layout->addRow(tr("Object:"), gridLayout);
+
+    m_searchObjectNameCheckBox = WidgetsFactory::createCheckBox(tr("Name"), this);
+    gridLayout->addWidget(m_searchObjectNameCheckBox, 0, 0);
+
+    m_searchObjectContentCheckBox = WidgetsFactory::createCheckBox(tr("Content"), this);
+    gridLayout->addWidget(m_searchObjectContentCheckBox, 0, 1);
+
+    m_searchObjectOutlineCheckBox = WidgetsFactory::createCheckBox(tr("Outline"), this);
+    gridLayout->addWidget(m_searchObjectOutlineCheckBox, 0, 2);
+
+    m_searchObjectTagCheckBox = WidgetsFactory::createCheckBox(tr("Tag"), this);
+    gridLayout->addWidget(m_searchObjectTagCheckBox, 1, 0);
+
+    m_searchObjectPathCheckBox = WidgetsFactory::createCheckBox(tr("Path"), this);
+    gridLayout->addWidget(m_searchObjectPathCheckBox, 1, 1);
+}
+
+void SearchPanel::setupSearchTarget(QFormLayout *p_layout)
+{
+    auto gridLayout = new QGridLayout();
+    p_layout->addRow(tr("Target:"), gridLayout);
+
+    m_searchTargetFileCheckBox = WidgetsFactory::createCheckBox(tr("File"), this);
+    gridLayout->addWidget(m_searchTargetFileCheckBox, 0, 0);
+
+    m_searchTargetFolderCheckBox = WidgetsFactory::createCheckBox(tr("Folder"), this);
+    gridLayout->addWidget(m_searchTargetFolderCheckBox, 0, 1);
+
+    m_searchTargetNotebookCheckBox = WidgetsFactory::createCheckBox(tr("Notebook"), this);
+    gridLayout->addWidget(m_searchTargetNotebookCheckBox, 0, 2);
+}
+
+void SearchPanel::setupFindOption(QFormLayout *p_layout)
+{
+    auto gridLayout = new QGridLayout();
+    p_layout->addRow(tr("Option:"), gridLayout);
+
+    m_caseSensitiveCheckBox = WidgetsFactory::createCheckBox(tr("&Case sensitive"), this);
+    gridLayout->addWidget(m_caseSensitiveCheckBox, 0, 0);
+
+    {
+        QButtonGroup *btnGroup = new QButtonGroup(this);
+
+        m_plainTextRadioBtn = WidgetsFactory::createRadioButton(tr("&Plain text"), this);
+        btnGroup->addButton(m_plainTextRadioBtn);
+        gridLayout->addWidget(m_plainTextRadioBtn, 1, 0);
+
+        m_wholeWordOnlyRadioBtn = WidgetsFactory::createRadioButton(tr("&Whole word only"), this);
+        btnGroup->addButton(m_wholeWordOnlyRadioBtn);
+        gridLayout->addWidget(m_wholeWordOnlyRadioBtn, 1, 1);
+
+        m_fuzzySearchRadioBtn = WidgetsFactory::createRadioButton(tr("&Fuzzy search"), this);
+        btnGroup->addButton(m_fuzzySearchRadioBtn);
+        gridLayout->addWidget(m_fuzzySearchRadioBtn, 2, 0);
+
+        m_regularExpressionRadioBtn = WidgetsFactory::createRadioButton(tr("Re&gular expression"), this);
+        btnGroup->addButton(m_regularExpressionRadioBtn);
+        gridLayout->addWidget(m_regularExpressionRadioBtn, 2, 1);
+    }
+}
+
+void SearchPanel::initOptions()
+{
+    // Read it from config.
+    m_option = QSharedPointer<SearchOption>::create(ConfigMgr::getInst().getSessionConfig().getSearchOption());
+
+    connect(VNoteX::getInst().getMainWindow(), &MainWindow::mainWindowClosedOnQuit,
+            this, [this]() {
+                saveFields(*m_option);
+                ConfigMgr::getInst().getSessionConfig().setSearchOption(*m_option);
+            });
+}
+
+void SearchPanel::restoreFields(const SearchOption &p_option)
+{
+    m_keywordComboBox->setEditText(p_option.m_keyword);
+    m_filePatternComboBox->setEditText(p_option.m_filePattern);
+
+    {
+        int idx = m_searchScopeComboBox->findData(static_cast<int>(p_option.m_scope));
+        if (idx != -1) {
+            m_searchScopeComboBox->setCurrentIndex(idx);
+        }
+    }
+
+    {
+        m_searchObjectNameCheckBox->setChecked(p_option.m_objects & SearchObject::SearchName);
+        m_searchObjectContentCheckBox->setChecked(p_option.m_objects & SearchObject::SearchContent);
+        m_searchObjectOutlineCheckBox->setChecked(p_option.m_objects & SearchObject::SearchOutline);
+        m_searchObjectTagCheckBox->setChecked(p_option.m_objects & SearchObject::SearchTag);
+        m_searchObjectPathCheckBox->setChecked(p_option.m_objects & SearchObject::SearchPath);
+    }
+
+    {
+        m_searchTargetFileCheckBox->setChecked(p_option.m_targets & SearchTarget::SearchFile);
+        m_searchTargetFolderCheckBox->setChecked(p_option.m_targets & SearchTarget::SearchFolder);
+        m_searchTargetNotebookCheckBox->setChecked(p_option.m_targets & SearchTarget::SearchNotebook);
+    }
+
+    {
+        m_plainTextRadioBtn->setChecked(true);
+
+        m_caseSensitiveCheckBox->setChecked(p_option.m_findOptions & FindOption::CaseSensitive);
+        m_wholeWordOnlyRadioBtn->setChecked(p_option.m_findOptions & FindOption::WholeWordOnly);
+        m_fuzzySearchRadioBtn->setChecked(p_option.m_findOptions & FindOption::FuzzySearch);
+        m_regularExpressionRadioBtn->setChecked(p_option.m_findOptions & FindOption::RegularExpression);
+    }
+}
+
+void SearchPanel::updateUIOnSearch()
+{
+    if (m_searchOngoing) {
+        m_progressBar->setMaximum(0);
+        m_progressBar->show();
+    } else {
+        m_progressBar->hide();
+    }
+}
+
+void SearchPanel::startSearch()
+{
+    if (m_searchOngoing) {
+        return;
+    }
+
+    // On start.
+    {
+        clearLog();
+        m_searchOngoing = true;
+        updateUIOnSearch();
+
+        prepareLocationList();
+    }
+
+    saveFields(*m_option);
+
+    auto state = search(m_option);
+
+    // On end.
+    handleSearchFinished(state);
+}
+
+void SearchPanel::handleSearchFinished(SearchState p_state)
+{
+    Q_ASSERT(m_searchOngoing);
+    Q_ASSERT(p_state != SearchState::Idle);
+
+    if (p_state != SearchState::Busy) {
+        appendLog(tr("Search finished: %1").arg(SearchStateToString(p_state)));
+
+        getSearcher()->clear();
+        m_searchOngoing = false;
+        updateUIOnSearch();
+    }
+}
+
+void SearchPanel::stopSearch()
+{
+    if (!m_searchOngoing) {
+        return;
+    }
+
+    getSearcher()->stop();
+}
+
+void SearchPanel::appendLog(const QString &p_text)
+{
+    if (p_text.isEmpty()) {
+        return;
+    }
+
+    if (!m_infoTextEdit) {
+        m_infoTextEdit = WidgetsFactory::createPlainTextConsole(this);
+        m_infoTextEdit->setMaximumHeight(m_infoTextEdit->minimumSizeHint().height());
+        static_cast<QVBoxLayout *>(layout())->insertWidget(layout()->count() - 1, m_infoTextEdit);
+    }
+
+    m_infoTextEdit->appendPlainText(">>> " + p_text);
+    m_infoTextEdit->ensureCursorVisible();
+    m_infoTextEdit->show();
+
+    QCoreApplication::sendPostedEvents();
+}
+
+void SearchPanel::clearLog()
+{
+    if (!m_infoTextEdit) {
+        return;
+    }
+
+    m_infoTextEdit->clear();
+    m_infoTextEdit->hide();
+}
+
+void SearchPanel::saveFields(SearchOption &p_option)
+{
+    p_option.m_keyword = m_keywordComboBox->currentText().trimmed();
+    p_option.m_filePattern = m_filePatternComboBox->currentText().trimmed();
+    p_option.m_scope = static_cast<SearchScope>(m_searchScopeComboBox->currentData().toInt());
+
+    {
+        p_option.m_objects = SearchObject::ObjectNone;
+        if (m_searchObjectNameCheckBox->isChecked()) {
+            p_option.m_objects |= SearchObject::SearchName;
+        }
+        if (m_searchObjectContentCheckBox->isChecked()) {
+            p_option.m_objects |= SearchObject::SearchContent;
+        }
+        if (m_searchObjectOutlineCheckBox->isChecked()) {
+            p_option.m_objects |= SearchObject::SearchOutline;
+        }
+        if (m_searchObjectTagCheckBox->isChecked()) {
+            p_option.m_objects |= SearchObject::SearchTag;
+        }
+        if (m_searchObjectPathCheckBox->isChecked()) {
+            p_option.m_objects |= SearchObject::SearchPath;
+        }
+    }
+
+    {
+        p_option.m_targets = SearchTarget::TargetNone;
+        if (m_searchTargetFileCheckBox->isChecked()) {
+            p_option.m_targets |= SearchTarget::SearchFile;
+        }
+        if (m_searchTargetFolderCheckBox->isChecked()) {
+            p_option.m_targets |= SearchTarget::SearchFolder;
+        }
+        if (m_searchTargetNotebookCheckBox->isChecked()) {
+            p_option.m_targets |= SearchTarget::SearchNotebook;
+        }
+    }
+
+    p_option.m_engine = SearchEngine::Internal;
+
+    {
+        p_option.m_findOptions = FindOption::FindNone;
+        if (m_caseSensitiveCheckBox->isChecked()) {
+            p_option.m_findOptions |= FindOption::CaseSensitive;
+        }
+        if (m_wholeWordOnlyRadioBtn->isChecked()) {
+            p_option.m_findOptions |= FindOption::WholeWordOnly;
+        }
+        if (m_fuzzySearchRadioBtn->isChecked()) {
+            p_option.m_findOptions |= FindOption::FuzzySearch;
+        }
+        if (m_regularExpressionRadioBtn->isChecked()) {
+            p_option.m_findOptions |= FindOption::RegularExpression;
+        }
+    }
+}
+
+SearchState SearchPanel::search(const QSharedPointer<SearchOption> &p_option)
+{
+    if (!isSearchOptionValid(*p_option)) {
+        return SearchState::Failed;
+    }
+
+    SearchState state = SearchState::Finished;
+
+    switch (p_option->m_scope) {
+    case SearchScope::Buffers:
+    {
+        auto buffers = m_provider->getBuffers();
+        if (buffers.isEmpty()) {
+            break;
+        }
+        state = getSearcher()->search(p_option, buffers);
+        break;
+    }
+
+    case SearchScope::CurrentFolder:
+    {
+        auto notebook = m_provider->getCurrentNotebook();
+        if (!notebook) {
+            break;
+        }
+        auto folder = m_provider->getCurrentFolder();
+        if (folder && (folder->isRoot() || notebook->isRecycleBinNode(folder))) {
+            folder = nullptr;
+        }
+        if (!folder) {
+            break;
+        }
+
+        state = getSearcher()->search(p_option, folder);
+        break;
+    }
+
+    case SearchScope::CurrentNotebook:
+    {
+        auto notebook = m_provider->getCurrentNotebook();
+        if (!notebook) {
+            break;
+        }
+
+        QVector<Notebook *> notebooks;
+        notebooks.push_back(notebook);
+        state = getSearcher()->search(p_option, notebooks);
+        break;
+    }
+
+    case SearchScope::AllNotebooks:
+    {
+        auto notebooks = m_provider->getNotebooks();
+        if (notebooks.isEmpty()) {
+            break;
+        }
+
+        state = getSearcher()->search(p_option, notebooks);
+        break;
+    }
+    }
+
+    return state;
+}
+
+bool SearchPanel::isSearchOptionValid(const SearchOption &p_option)
+{
+    if (p_option.m_keyword.isEmpty()) {
+        appendLog(tr("Invalid keyword"));
+        return false;
+    }
+
+    if (p_option.m_objects == SearchObject::ObjectNone) {
+        appendLog(tr("No object specified"));
+        return false;
+    }
+
+    if (p_option.m_targets == SearchTarget::TargetNone) {
+        appendLog(tr("No target specified"));
+        return false;
+    }
+
+    if (p_option.m_findOptions & FindOption::FuzzySearch
+        && p_option.m_objects & SearchObject::SearchContent) {
+        appendLog(tr("Fuzzy search is not allowed when searching content"));
+        return false;
+    }
+
+    return true;
+}
+
+Searcher *SearchPanel::getSearcher()
+{
+    if (!m_searcher) {
+        m_searcher = new Searcher(this);
+        connect(m_searcher, &Searcher::progressUpdated,
+                this, &SearchPanel::updateProgress);
+        connect(m_searcher, &Searcher::logRequested,
+                this, &SearchPanel::appendLog);
+        connect(m_searcher, &Searcher::resultItemAdded,
+                this, [this](const QSharedPointer<SearchResultItem> &p_item) {
+                    m_locationList->addLocation(p_item->m_location);
+                });
+        connect(m_searcher, &Searcher::resultItemsAdded,
+                this, [this](const QVector<QSharedPointer<SearchResultItem>> &p_items) {
+                    for (const auto &item : p_items) {
+                        m_locationList->addLocation(item->m_location);
+                    }
+                });
+        connect(m_searcher, &Searcher::finished,
+                this, &SearchPanel::handleSearchFinished);
+    }
+    return m_searcher;
+}
+
+void SearchPanel::updateProgress(int p_val, int p_maximum)
+{
+    m_progressBar->setMaximum(p_maximum);
+    m_progressBar->setValue(p_val);
+}
+
+void SearchPanel::prepareLocationList()
+{
+    auto mainWindow = VNoteX::getInst().getMainWindow();
+    mainWindow->setLocationListVisible(true);
+
+    if (!m_locationList) {
+        m_locationList = mainWindow->getLocationList();
+    }
+
+    m_locationList->clear();
+    m_locationList->startSession([this](const Location &p_location) {
+                handleLocationActivated(p_location);
+            });
+}
+
+void SearchPanel::handleLocationActivated(const Location &p_location)
+{
+    qDebug() << "location activated" << p_location;
+    // TODO: decode the path of location and handle different types of destination.
+    auto paras = QSharedPointer<FileOpenParameters>::create();
+    paras->m_lineNumber = p_location.m_lineNumber;
+    emit VNoteX::getInst().openFileRequested(p_location.m_path, paras);
+}

+ 144 - 0
src/widgets/searchpanel.h

@@ -0,0 +1,144 @@
+#ifndef SEARCHPANEL_H
+#define SEARCHPANEL_H
+
+#include <QFrame>
+#include <QSharedPointer>
+#include <QList>
+
+#include <search/searchdata.h>
+#include <search/searcher.h>
+
+class QComboBox;
+class QCheckBox;
+class QFormLayout;
+class QProgressBar;
+class QToolButton;
+class QPlainTextEdit;
+class QRadioButton;
+class QButtonGroup;
+
+namespace vnotex
+{
+    class TitleBar;
+    class Buffer;
+    class Node;
+    class Notebook;
+    class LocationList;
+    struct Location;
+
+    class ISearchInfoProvider
+    {
+    public:
+        ISearchInfoProvider() = default;
+
+        virtual ~ISearchInfoProvider() = default;
+
+        virtual QList<Buffer *> getBuffers() const = 0;
+
+        virtual Node *getCurrentFolder() const = 0;
+
+        virtual Notebook *getCurrentNotebook() const = 0;
+
+        virtual QVector<Notebook *> getNotebooks() const = 0;
+    };
+
+    class SearchPanel : public QFrame
+    {
+        Q_OBJECT
+    public:
+        explicit SearchPanel(const QSharedPointer<ISearchInfoProvider> &p_provider, QWidget *p_parent = nullptr);
+
+    private slots:
+        void startSearch();
+
+        void stopSearch();
+
+        void handleSearchFinished(SearchState p_state);
+
+        void updateProgress(int p_val, int p_maximum);
+
+        void appendLog(const QString &p_text);
+
+    private:
+        void setupUI();
+
+        TitleBar *setupTitleBar(const QString &p_title, QWidget *p_parent = nullptr);
+
+        void setupSearchObject(QFormLayout *p_layout);
+
+        void setupSearchTarget(QFormLayout *p_layout);
+
+        void setupFindOption(QFormLayout *p_layout);
+
+        void initOptions();
+
+        void restoreFields(const SearchOption &p_option);
+
+        void saveFields(SearchOption &p_option);
+
+        void updateUIOnSearch();
+
+        void clearLog();
+
+        SearchState search(const QSharedPointer<SearchOption> &p_option);
+
+        bool isSearchOptionValid(const SearchOption &p_option);
+
+        Searcher *getSearcher();
+
+        void prepareLocationList();
+
+        void handleLocationActivated(const Location &p_location);
+
+        QSharedPointer<ISearchInfoProvider> m_provider;
+
+        QToolButton *m_searchBtn = nullptr;
+
+        QComboBox *m_keywordComboBox = nullptr;
+
+        QComboBox *m_searchScopeComboBox = nullptr;
+
+        QCheckBox *m_searchObjectNameCheckBox = nullptr;
+
+        QCheckBox *m_searchObjectContentCheckBox = nullptr;
+
+        QCheckBox *m_searchObjectOutlineCheckBox = nullptr;
+
+        QCheckBox *m_searchObjectTagCheckBox = nullptr;
+
+        QCheckBox *m_searchObjectPathCheckBox = nullptr;
+
+        QCheckBox *m_searchTargetFileCheckBox = nullptr;
+
+        QCheckBox *m_searchTargetFolderCheckBox = nullptr;
+
+        QCheckBox *m_searchTargetNotebookCheckBox = nullptr;
+
+        QComboBox *m_filePatternComboBox = nullptr;
+
+        QCheckBox *m_caseSensitiveCheckBox = nullptr;
+
+        // WholeWordOnly/RegularExpression/FuzzySearch is exclusive.
+        QRadioButton *m_plainTextRadioBtn = nullptr;
+
+        QRadioButton *m_wholeWordOnlyRadioBtn = nullptr;
+
+        QRadioButton *m_fuzzySearchRadioBtn = nullptr;
+
+        QRadioButton *m_regularExpressionRadioBtn = nullptr;
+
+        QProgressBar *m_progressBar = nullptr;
+
+        QPlainTextEdit *m_infoTextEdit = nullptr;
+
+        QSharedPointer<SearchOption> m_option;
+
+        bool m_searchOngoing = false;
+
+        Searcher *m_searcher = nullptr;
+
+        LocationList *m_locationList = nullptr;
+    };
+}
+
+#endif // SEARCHPANEL_H

+ 29 - 6
src/widgets/titlebar.cpp

@@ -23,19 +23,21 @@ const QString TitleBar::c_menuIconForegroundName = "widgets#titlebar#menu_icon#f
 const QString TitleBar::c_menuIconDisabledForegroundName = "widgets#titlebar#menu_icon#disabled#fg";
 
 TitleBar::TitleBar(const QString &p_title,
+                   bool p_hasInfoLabel,
                    TitleBar::Actions p_actionFlags,
                    QWidget *p_parent)
     : QWidget(p_parent)
 {
-    setupUI(p_title, p_actionFlags);
+    setupUI(p_title, p_hasInfoLabel, p_actionFlags);
 }
 
-void TitleBar::setupUI(const QString &p_title, TitleBar::Actions p_actionFlags)
+void TitleBar::setupUI(const QString &p_title, bool p_hasInfoLabel, TitleBar::Actions p_actionFlags)
 {
     auto mainLayout = new QHBoxLayout(this);
     mainLayout->setContentsMargins(0, 0, 0, 0);
 
     // Title label.
+    // Should always add it even if title is empty. Otherwise, we could not catch the hover event to show actions.
     {
         auto titleLabel = new QLabel(p_title, this);
         titleLabel->setProperty(c_titleProp, true);
@@ -56,13 +58,20 @@ void TitleBar::setupUI(const QString &p_title, TitleBar::Actions p_actionFlags)
         setActionButtonsVisible(false);
     }
 
+    // Info label.
+    if (p_hasInfoLabel) {
+        m_infoLabel = new QLabel(this);
+        m_infoLabel->setProperty(c_titleProp, true);
+        mainLayout->addWidget(m_infoLabel);
+    }
+
     setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
 }
 
 QToolButton *TitleBar::newActionButton(const QString &p_iconName, const QString &p_text, QWidget *p_parent)
 {
     auto btn = new QToolButton(p_parent);
-    btn->setProperty(PropertyDefs::s_actionToolButton, true);
+    btn->setProperty(PropertyDefs::c_actionToolButton, true);
 
     const auto &themeMgr = VNoteX::getInst().getThemeMgr();
     auto iconFile = themeMgr.getIconFile(p_iconName);
@@ -99,7 +108,7 @@ void TitleBar::enterEvent(QEvent *p_event)
 void TitleBar::leaveEvent(QEvent *p_event)
 {
     QWidget::leaveEvent(p_event);
-    setActionButtonsVisible(m_alwaysShowActionButtons);
+    setActionButtonsVisible(m_actionButtonsForcedShown || m_actionButtonsAlwaysShown);
 }
 
 void TitleBar::setActionButtonsVisible(bool p_visible)
@@ -165,12 +174,12 @@ QToolButton *TitleBar::addActionButton(const QString &p_iconName, const QString
     btn->setMenu(p_menu);
     connect(p_menu, &QMenu::aboutToShow,
             this, [this]() {
-                m_alwaysShowActionButtons = true;
+                m_actionButtonsForcedShown = true;
                 setActionButtonsVisible(true);
             });
     connect(p_menu, &QMenu::aboutToHide,
             this, [this]() {
-                m_alwaysShowActionButtons = false;
+                m_actionButtonsForcedShown = false;
                 setActionButtonsVisible(false);
             });
     return btn;
@@ -180,3 +189,17 @@ QHBoxLayout *TitleBar::actionButtonLayout() const
 {
     return static_cast<QHBoxLayout *>(m_buttonWidget->layout());
 }
+
+void TitleBar::setInfoLabel(const QString &p_info)
+{
+    Q_ASSERT(m_infoLabel);
+    if (m_infoLabel) {
+        m_infoLabel->setText(p_info);
+    }
+}
+
+void TitleBar::setActionButtonsAlwaysShown(bool p_shown)
+{
+    m_actionButtonsAlwaysShown = p_shown;
+    setActionButtonsVisible(m_actionButtonsForcedShown || m_actionButtonsAlwaysShown);
+}

+ 12 - 2
src/widgets/titlebar.h

@@ -7,6 +7,7 @@
 
 class QToolButton;
 class QHBoxLayout;
+class QLabel;
 
 namespace vnotex
 {
@@ -23,6 +24,7 @@ namespace vnotex
         Q_DECLARE_FLAGS(Actions, Action)
 
         TitleBar(const QString &p_title,
+                 bool p_hasInfoLabel,
                  TitleBar::Actions p_actionFlags,
                  QWidget *p_parent = nullptr);
 
@@ -46,13 +48,17 @@ namespace vnotex
 
         void addMenuSeparator();
 
+        void setInfoLabel(const QString &p_info);
+
+        void setActionButtonsAlwaysShown(bool p_shown);
+
     protected:
         void enterEvent(QEvent *p_event) Q_DECL_OVERRIDE;
 
         void leaveEvent(QEvent *p_event) Q_DECL_OVERRIDE;
 
     private:
-        void setupUI(const QString &p_title, TitleBar::Actions p_actionFlags);
+        void setupUI(const QString &p_title, bool p_hasInfoLabel, TitleBar::Actions p_actionFlags);
 
         void setupActionButtons(TitleBar::Actions p_actionFlags);
 
@@ -64,11 +70,15 @@ namespace vnotex
 
         static QIcon generateMenuActionIcon(const QString &p_iconName);
 
+        QLabel *m_infoLabel = nullptr;
+
         QVector<QToolButton *> m_actionButtons;
 
         QWidget *m_buttonWidget = nullptr;
 
-        bool m_alwaysShowActionButtons = false;
+        bool m_actionButtonsAlwaysShown = false;
+
+        bool m_actionButtonsForcedShown = false;
 
         QMenu *m_menu = nullptr;
 

+ 1 - 1
src/widgets/titletoolbar.cpp

@@ -92,7 +92,7 @@ void TitleToolBar::addTitleBarIcons(const QIcon &p_minimizeIcon,
                                       m_window->close();
                                   });
         auto btn = static_cast<QToolButton *>(widgetForAction(closeAct));
-        btn->setProperty(PropertyDefs::s_dangerButton, true);
+        btn->setProperty(PropertyDefs::c_dangerButton, true);
     }
 
     updateMaximizeAct();

+ 5 - 4
src/widgets/toolbarhelper.cpp

@@ -48,8 +48,9 @@ QToolBar *ToolBarHelper::setupFileToolBar(MainWindow *p_win, QToolBar *p_toolBar
 
         auto toolBtn = dynamic_cast<QToolButton *>(tb->widgetForAction(act));
         Q_ASSERT(toolBtn);
+        toolBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
         toolBtn->setPopupMode(QToolButton::InstantPopup);
-        toolBtn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true);
+        toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
 
         auto newMenu = WidgetsFactory::createMenu(tb);
         toolBtn->setMenu(newMenu);
@@ -149,7 +150,7 @@ QToolBar *ToolBarHelper::setupFileToolBar(MainWindow *p_win, QToolBar *p_toolBar
         auto btn = dynamic_cast<QToolButton *>(tb->widgetForAction(act));
         Q_ASSERT(btn);
         btn->setPopupMode(QToolButton::InstantPopup);
-        btn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true);
+        btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
 
         auto newMenu = WidgetsFactory::createMenu(tb);
         btn->setMenu(newMenu);
@@ -269,7 +270,7 @@ QToolBar *ToolBarHelper::setupSettingsToolBar(MainWindow *p_win, QToolBar *p_too
         auto btn = dynamic_cast<QToolButton *>(tb->widgetForAction(act));
         Q_ASSERT(btn);
         btn->setPopupMode(QToolButton::InstantPopup);
-        btn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true);
+        btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
 
         auto menu = WidgetsFactory::createMenu(tb);
         btn->setMenu(menu);
@@ -348,7 +349,7 @@ QToolBar *ToolBarHelper::setupSettingsToolBar(MainWindow *p_win, QToolBar *p_too
         auto btn = dynamic_cast<QToolButton *>(tb->widgetForAction(act));
         Q_ASSERT(btn);
         btn->setPopupMode(QToolButton::InstantPopup);
-        btn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true);
+        btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
 
         auto menu = WidgetsFactory::createMenu(tb);
         btn->setMenu(menu);

+ 24 - 4
src/widgets/viewarea.cpp

@@ -10,6 +10,7 @@
 #include <QDropEvent>
 #include <QTimer>
 #include <QApplication>
+#include <QSet>
 
 #include "viewwindow.h"
 #include "mainwindow.h"
@@ -655,9 +656,7 @@ bool ViewArea::removeWorkspaceInViewSplit(ViewSplit *p_split, bool p_insertNew)
 {
     // Close all the ViewWindows.
     setCurrentViewSplit(p_split, true);
-    auto wins = getAllViewWindows(p_split, [](ViewWindow *) {
-                return true;
-            });
+    auto wins = getAllViewWindows(p_split);
     for (const auto win : wins) {
         if (!closeViewWindow(win, false, false)) {
             return false;
@@ -963,7 +962,7 @@ void ViewArea::dropEvent(QDropEvent *p_event)
     QWidget::dropEvent(p_event);
 }
 
-QVector<ViewWindow *> ViewArea::getAllViewWindows(ViewSplit *p_split, const ViewSplit::ViewWindowSelector &p_func)
+QVector<ViewWindow *> ViewArea::getAllViewWindows(ViewSplit *p_split, const ViewSplit::ViewWindowSelector &p_func) const
 {
     QVector<ViewWindow *> wins;
     p_split->forEachViewWindow([p_func, &wins](ViewWindow *p_win) {
@@ -974,3 +973,24 @@ QVector<ViewWindow *> ViewArea::getAllViewWindows(ViewSplit *p_split, const View
         });
     return wins;
 }
+
+QVector<ViewWindow *> ViewArea::getAllViewWindows(ViewSplit *p_split) const
+{
+   return getAllViewWindows(p_split, [](ViewWindow *) {
+              return true;
+          });
+}
+
+QList<Buffer *> ViewArea::getAllBuffersInViewSplits() const
+{
+    QSet<Buffer *> bufferSet;
+
+    for (auto split : m_splits) {
+        auto wins = getAllViewWindows(split);
+        for (auto win : wins) {
+            bufferSet.insert(win->getBuffer());
+        }
+    }
+
+    return bufferSet.values();
+}

+ 6 - 1
src/widgets/viewarea.h

@@ -68,6 +68,9 @@ namespace vnotex
 
         QSize sizeHint() const Q_DECL_OVERRIDE;
 
+        // Not all Workspace. Just all ViewSplits.
+        QList<Buffer *> getAllBuffersInViewSplits() const;
+
     public slots:
         void openBuffer(Buffer *p_buffer, const QSharedPointer<FileOpenParameters> &p_paras);
 
@@ -198,7 +201,9 @@ namespace vnotex
 
         void checkCurrentViewWindowChange();
 
-        QVector<ViewWindow *> getAllViewWindows(ViewSplit *p_split, const ViewSplit::ViewWindowSelector &p_func);
+        QVector<ViewWindow *> getAllViewWindows(ViewSplit *p_split, const ViewSplit::ViewWindowSelector &p_func) const;
+
+        QVector<ViewWindow *> getAllViewWindows(ViewSplit *p_split) const;
 
         QLayout *m_mainLayout = nullptr;
 

+ 4 - 4
src/widgets/viewsplit.cpp

@@ -109,7 +109,7 @@ void ViewSplit::setupCornerWidget()
 
     // Container.
     auto widget = new QWidget(this);
-    widget->setProperty(PropertyDefs::s_viewSplitCornerWidget, true);
+    widget->setProperty(PropertyDefs::c_viewSplitCornerWidget, true);
     auto layout = new QHBoxLayout(widget);
     layout->setContentsMargins(0, 0, 0, 0);
 
@@ -117,7 +117,7 @@ void ViewSplit::setupCornerWidget()
     {
         m_windowListButton = new QToolButton(this);
         m_windowListButton->setPopupMode(QToolButton::InstantPopup);
-        m_windowListButton->setProperty(PropertyDefs::s_actionToolButton, true);
+        m_windowListButton->setProperty(PropertyDefs::c_actionToolButton, true);
 
         auto act = new QAction(s_windowListIcon, tr("Windows List"), m_windowListButton);
         m_windowListButton->setDefaultAction(act);
@@ -141,7 +141,7 @@ void ViewSplit::setupCornerWidget()
     {
         m_menuButton = new QToolButton(this);
         m_menuButton->setPopupMode(QToolButton::InstantPopup);
-        m_menuButton->setProperty(PropertyDefs::s_actionToolButton, true);
+        m_menuButton->setProperty(PropertyDefs::c_actionToolButton, true);
 
         auto act = new QAction(s_menuIcon, tr("Workspaces and Splits"), m_menuButton);
         m_menuButton->setDefaultAction(act);
@@ -607,7 +607,7 @@ void ViewSplit::mousePressEvent(QMouseEvent *p_event)
     emit focused(this);
 }
 
-bool ViewSplit::forEachViewWindow(const ViewWindowSelector &p_func)
+bool ViewSplit::forEachViewWindow(const ViewWindowSelector &p_func) const
 {
     int cnt = getViewWindowCount();
     for (int i = 0; i < cnt; ++i) {

+ 1 - 1
src/widgets/viewsplit.h

@@ -56,7 +56,7 @@ namespace vnotex
 
         // @p_func: return true if going well, return false to stop the iteration.
         // Return false if there is a break.
-        bool forEachViewWindow(const ViewWindowSelector &p_func);
+        bool forEachViewWindow(const ViewWindowSelector &p_func) const;
 
         QVector<ViewWindowNavigationModeInfo> getNavigationModeInfo() const;
 

+ 2 - 2
src/widgets/viewwindow.cpp

@@ -128,7 +128,7 @@ void ViewWindow::initIcons()
     }
 
     const auto &themeMgr = VNoteX::getInst().getThemeMgr();
-    const QString savedIconName("saved.svg");
+    const QString savedIconName("buffer.svg");
     const QString unsavedIconFg("base#icon#warning#fg");
     s_savedIcon = IconUtils::fetchIcon(themeMgr.getIconFile(savedIconName));
     s_modifiedIcon = IconUtils::fetchIcon(themeMgr.getIconFile(savedIconName),
@@ -1083,6 +1083,6 @@ void ViewWindow::read(bool p_save)
 QToolBar *ViewWindow::createToolBar(QWidget *p_parent)
 {
     auto toolBar = new QToolBar(p_parent);
-    toolBar->setProperty(PropertyDefs::s_viewWindowToolBar, true);
+    toolBar->setProperty(PropertyDefs::c_viewWindowToolBar, true);
     return toolBar;
 }

+ 4 - 4
src/widgets/viewwindowtoolbarhelper.cpp

@@ -121,7 +121,7 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action)
         auto toolBtn = dynamic_cast<QToolButton *>(p_tb->widgetForAction(act));
         Q_ASSERT(toolBtn);
         toolBtn->setPopupMode(QToolButton::InstantPopup);
-        toolBtn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true);
+        toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
 
         auto menu = WidgetsFactory::createMenu(p_tb);
 
@@ -282,7 +282,7 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action)
         auto toolBtn = dynamic_cast<QToolButton *>(p_tb->widgetForAction(act));
         Q_ASSERT(toolBtn);
         toolBtn->setPopupMode(QToolButton::InstantPopup);
-        toolBtn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true);
+        toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
 
         auto menu = new AttachmentPopup(toolBtn, p_tb);
         toolBtn->setMenu(menu);
@@ -297,7 +297,7 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action)
         auto toolBtn = dynamic_cast<QToolButton *>(p_tb->widgetForAction(act));
         Q_ASSERT(toolBtn);
         toolBtn->setPopupMode(QToolButton::InstantPopup);
-        toolBtn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true);
+        toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
 
         addButtonShortcut(toolBtn, editorConfig.getShortcut(Shortcut::Outline), viewWindow);
 
@@ -322,7 +322,7 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action)
         auto toolBtn = dynamic_cast<QToolButton *>(p_tb->widgetForAction(act));
         Q_ASSERT(toolBtn);
         toolBtn->setPopupMode(QToolButton::InstantPopup);
-        toolBtn->setProperty(PropertyDefs::s_toolButtonWithoutMenuIndicator, true);
+        toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
 
         auto menu = WidgetsFactory::createMenu(p_tb);
 

+ 6 - 0
src/widgets/widgets.pri

@@ -43,6 +43,7 @@ SOURCES += \
     $$PWD/lineedit.cpp \
     $$PWD/lineeditdelegate.cpp \
     $$PWD/listwidget.cpp \
+    $$PWD/locationlist.cpp \
     $$PWD/mainwindow.cpp \
     $$PWD/markdownviewwindow.cpp \
     $$PWD/navigationmodemgr.cpp \
@@ -50,6 +51,8 @@ SOURCES += \
     $$PWD/outlineprovider.cpp \
     $$PWD/outlineviewer.cpp \
     $$PWD/propertydefs.cpp \
+    $$PWD/searchinfoprovider.cpp \
+    $$PWD/searchpanel.cpp \
     $$PWD/systemtrayhelper.cpp \
     $$PWD/textviewwindow.cpp \
     $$PWD/toolbarhelper.cpp \
@@ -130,6 +133,7 @@ HEADERS += \
     $$PWD/lineedit.h \
     $$PWD/lineeditdelegate.h \
     $$PWD/listwidget.h \
+    $$PWD/locationlist.h \
     $$PWD/mainwindow.h \
     $$PWD/markdownviewwindow.h \
     $$PWD/navigationmodemgr.h \
@@ -138,6 +142,8 @@ HEADERS += \
     $$PWD/outlineprovider.h \
     $$PWD/outlineviewer.h \
     $$PWD/propertydefs.h \
+    $$PWD/searchinfoprovider.h \
+    $$PWD/searchpanel.h \
     $$PWD/systemtrayhelper.h \
     $$PWD/textviewwindow.h \
     $$PWD/textviewwindowhelper.h \

+ 6 - 0
src/widgets/widgetsfactory.cpp

@@ -9,6 +9,7 @@
 #include <QToolButton>
 #include <QFormLayout>
 #include <QPlainTextEdit>
+#include <QRadioButton>
 
 #include "lineedit.h"
 #include "combobox.h"
@@ -52,6 +53,11 @@ QCheckBox *WidgetsFactory::createCheckBox(const QString &p_text, QWidget *p_pare
     return new QCheckBox(p_text, p_parent);
 }
 
+QRadioButton *WidgetsFactory::createRadioButton(const QString &p_text, QWidget *p_parent)
+{
+    return new QRadioButton(p_text, p_parent);
+}
+
 QSpinBox *WidgetsFactory::createSpinBox(QWidget *p_parent)
 {
     return new QSpinBox(p_parent);

+ 3 - 0
src/widgets/widgetsfactory.h

@@ -12,6 +12,7 @@ class QToolButton;
 class QDoubleSpinBox;
 class QFormLayout;
 class QPlainTextEdit;
+class QRadioButton;
 
 namespace vnotex
 {
@@ -32,6 +33,8 @@ namespace vnotex
 
         static QCheckBox *createCheckBox(const QString &p_text, QWidget *p_parent = nullptr);
 
+        static QRadioButton *createRadioButton(const QString &p_text, QWidget *p_parent = nullptr);
+
         static QSpinBox *createSpinBox(QWidget *p_parent = nullptr);
 
         static QDoubleSpinBox *createDoubleSpinBox(QWidget *p_parent = nullptr);

+ 1 - 0
tests/test_core/test_notebook/test_notebook.pro

@@ -18,6 +18,7 @@ include($$CORE_FOLDER/core.pri)
 include($$SRC_FOLDER/widgets/widgets.pri)
 include($$SRC_FOLDER/utils/utils.pri)
 include($$SRC_FOLDER/export/export.pri)
+include($$SRC_FOLDER/search/search.pri)
 
 SOURCES += \
     test_notebook.cpp