Browse Source

UnitedEntry: support United Entry

Le Tan 3 years ago
parent
commit
dba9fb30e8
68 changed files with 2475 additions and 205 deletions
  1. 1 1
      src/commandlineoptions.cpp
  2. 32 0
      src/core/coreconfig.cpp
  3. 10 0
      src/core/coreconfig.h
  4. 1 3
      src/core/vnotex.cpp
  5. 5 5
      src/core/vnotex.h
  6. 13 0
      src/core/widgetconfig.cpp
  7. 5 0
      src/core/widgetconfig.h
  8. 2 0
      src/data/core/core.qrc
  9. 2 0
      src/data/core/icons/other_item.svg
  10. 1 0
      src/data/core/icons/united_entry.svg
  11. 70 4
      src/data/core/vnotex.json
  12. 6 2
      src/data/extra/themes/moonlight/interface.qss
  13. 11 0
      src/data/extra/themes/moonlight/palette.json
  14. 4 0
      src/data/extra/themes/native/interface.qss
  15. 11 0
      src/data/extra/themes/native/palette.json
  16. 6 2
      src/data/extra/themes/pure/interface.qss
  17. 11 0
      src/data/extra/themes/pure/palette.json
  18. 6 2
      src/data/extra/themes/solarized-dark/interface.qss
  19. 11 0
      src/data/extra/themes/solarized-dark/palette.json
  20. 6 2
      src/data/extra/themes/solarized-light/interface.qss
  21. 11 0
      src/data/extra/themes/solarized-light/palette.json
  22. 6 2
      src/data/extra/themes/vscode-dark/interface.qss
  23. 11 0
      src/data/extra/themes/vscode-dark/palette.json
  24. 30 0
      src/search/isearchinfoprovider.h
  25. 3 0
      src/search/search.pri
  26. 5 0
      src/search/searchdata.cpp
  27. 2 0
      src/search/searchdata.h
  28. 99 0
      src/search/searchhelper.cpp
  29. 28 0
      src/search/searchhelper.h
  30. 31 24
      src/search/searchtoken.cpp
  31. 3 3
      src/search/searchtoken.h
  32. 2 0
      src/src.pro
  33. 86 0
      src/unitedentry/entrypopup.cpp
  34. 34 0
      src/unitedentry/entrypopup.h
  35. 29 0
      src/unitedentry/entrywidgetfactory.cpp
  36. 23 0
      src/unitedentry/entrywidgetfactory.h
  37. 346 0
      src/unitedentry/findunitedentry.cpp
  38. 75 0
      src/unitedentry/findunitedentry.h
  39. 48 0
      src/unitedentry/helpunitedentry.cpp
  40. 31 0
      src/unitedentry/helpunitedentry.h
  41. 138 0
      src/unitedentry/iunitedentry.cpp
  42. 79 0
      src/unitedentry/iunitedentry.h
  43. 487 0
      src/unitedentry/unitedentry.cpp
  44. 85 0
      src/unitedentry/unitedentry.h
  45. 24 0
      src/unitedentry/unitedentry.pri
  46. 99 0
      src/unitedentry/unitedentryalias.cpp
  47. 56 0
      src/unitedentry/unitedentryalias.h
  48. 70 0
      src/unitedentry/unitedentryhelper.cpp
  49. 43 0
      src/unitedentry/unitedentryhelper.h
  50. 80 0
      src/unitedentry/unitedentrymgr.cpp
  51. 52 0
      src/unitedentry/unitedentrymgr.h
  52. 1 0
      src/utils/asyncworker.h
  53. 20 0
      src/utils/widgetutils.cpp
  54. 2 0
      src/utils/widgetutils.h
  55. 5 0
      src/widgets/lineedit.cpp
  56. 2 0
      src/widgets/lineedit.h
  57. 1 0
      src/widgets/locationlist.cpp
  58. 6 0
      src/widgets/mainwindow.cpp
  59. 2 0
      src/widgets/mainwindow.h
  60. 13 4
      src/widgets/searchinfoprovider.cpp
  61. 6 1
      src/widgets/searchinfoprovider.h
  62. 7 91
      src/widgets/searchpanel.cpp
  63. 1 24
      src/widgets/searchpanel.h
  64. 30 33
      src/widgets/toolbarhelper.cpp
  65. 0 2
      src/widgets/toolbarhelper.h
  66. 43 0
      src/widgets/treewidget.cpp
  67. 4 0
      src/widgets/treewidget.h
  68. 2 0
      tests/commonfull.pri

+ 1 - 1
src/commandlineoptions.cpp

@@ -27,7 +27,7 @@ CommandLineOptions::ParseResult CommandLineOptions::parse(const QStringList &p_a
     {
         QCommandLineOption webRemoteDebuggingPortOpt("remote-debugging-port",
                                                      MainWindow::tr("WebEngine remote debugging port."),
-                                                     "port_number");
+                                                     MainWindow::tr("port_number"));
         webRemoteDebuggingPortOpt.setFlags(QCommandLineOption::HiddenFromHelp);
         parser.addOption(webRemoteDebuggingPortOpt);
 

+ 32 - 0
src/core/coreconfig.cpp

@@ -88,6 +88,8 @@ void CoreConfig::init(const QJsonObject &p_app,
     }
 
     loadFileTypeSuffixes(appObj, userObj);
+
+    loadUnitedEntry(appObj, userObj);
 }
 
 QJsonObject CoreConfig::toJson() const
@@ -105,6 +107,7 @@ QJsonObject CoreConfig::toJson() const
     obj[QStringLiteral("per_notebook_history")] = m_perNotebookHistoryEnabled;
     obj[QStringLiteral("line_ending")] = lineEndingPolicyToString(m_lineEnding);
     obj[QStringLiteral("file_type_suffixes")] = saveFileTypeSuffixes();
+    obj[QStringLiteral("united_entry")] = saveUnitedEntry();
     return obj;
 }
 
@@ -297,6 +300,25 @@ QJsonArray CoreConfig::saveFileTypeSuffixes() const
     return arr;
 }
 
+void CoreConfig::loadUnitedEntry(const QJsonObject &p_app, const QJsonObject &p_user)
+{
+    QJsonObject unitedObj;
+    if (p_user.contains(QStringLiteral("united_entry"))) {
+        unitedObj = p_user[QStringLiteral("united_entry")].toObject();
+    } else {
+        unitedObj = p_app[QStringLiteral("united_entry")].toObject();
+    }
+
+    m_unitedEntryAlias = unitedObj[QStringLiteral("alias")].toArray();
+}
+
+QJsonObject CoreConfig::saveUnitedEntry() const
+{
+    QJsonObject unitedObj;
+    unitedObj[QStringLiteral("alias")] = m_unitedEntryAlias;
+    return unitedObj;
+}
+
 const QVector<CoreConfig::FileTypeSuffix> &CoreConfig::getFileTypeSuffixes() const
 {
     return m_fileTypeSuffixes;
@@ -321,3 +343,13 @@ const QStringList *CoreConfig::findFileTypeSuffix(const QString &p_name) const
 
     return nullptr;
 }
+
+const QJsonArray &CoreConfig::getUnitedEntryAlias() const
+{
+    return m_unitedEntryAlias;
+}
+
+void CoreConfig::setUnitedEntryAlias(const QJsonArray &p_alias)
+{
+    updateConfig(m_unitedEntryAlias, p_alias, this);
+}

+ 10 - 0
src/core/coreconfig.h

@@ -68,6 +68,7 @@ namespace vnotex
             MoveOneSplitUp,
             MoveOneSplitRight,
             OpenLastClosedFile,
+            UnitedEntry,
             MaxShortcut
         };
         Q_ENUM(Shortcut)
@@ -133,6 +134,9 @@ namespace vnotex
 
         const QStringList *findFileTypeSuffix(const QString &p_name) const;
 
+        const QJsonArray &getUnitedEntryAlias() const;
+        void setUnitedEntryAlias(const QJsonArray &p_alias);
+
     private:
         friend class MainConfig;
 
@@ -146,6 +150,10 @@ namespace vnotex
 
         QJsonArray saveFileTypeSuffixes() const;
 
+        void loadUnitedEntry(const QJsonObject &p_app, const QJsonObject &p_user);
+
+        QJsonObject saveUnitedEntry() const;
+
         // Theme name.
         QString m_theme;
 
@@ -181,6 +189,8 @@ namespace vnotex
 
         QVector<FileTypeSuffix> m_fileTypeSuffixes;
 
+        QJsonArray m_unitedEntryAlias;
+
         static QStringList s_availableLocales;
     };
 } // ns vnotex

+ 1 - 3
src/core/vnotex.cpp

@@ -20,9 +20,7 @@
 using namespace vnotex;
 
 VNoteX::VNoteX(QObject *p_parent)
-    : QObject(p_parent),
-      m_mainWindow(nullptr),
-      m_notebookMgr(nullptr)
+    : QObject(p_parent)
 {
     m_instanceId = QRandomGenerator::global()->generate64();
 

+ 5 - 5
src/core/vnotex.h

@@ -132,19 +132,19 @@ namespace vnotex
 
         void initQuickAccess();
 
-        MainWindow *m_mainWindow;
+        MainWindow *m_mainWindow = nullptr;
 
         // QObject managed.
-        ThemeMgr *m_themeMgr;
+        ThemeMgr *m_themeMgr = nullptr;
 
         // QObject managed.
-        TaskMgr *m_taskMgr;
+        TaskMgr *m_taskMgr = nullptr;
 
         // QObject managed.
-        NotebookMgr *m_notebookMgr;
+        NotebookMgr *m_notebookMgr = nullptr;
 
         // QObject managed.
-        BufferMgr *m_bufferMgr;
+        BufferMgr *m_bufferMgr = nullptr;
 
         // Used to identify app's instance.
         ID m_instanceId = 0;

+ 13 - 0
src/core/widgetconfig.cpp

@@ -46,6 +46,8 @@ void WidgetConfig::init(const QJsonObject &p_app,
     m_tagExplorerTwoColumnsEnabled = READBOOL(QStringLiteral("tag_explorer_two_columns_enabled"));
 
     m_newNoteDefaultFileType = READINT(QStringLiteral("new_note_default_file_type"));
+
+    m_unitedEntryExpandAllEnabled = READBOOL(QStringLiteral("united_entry_expand_all"));
 }
 
 QJsonObject WidgetConfig::toJson() const
@@ -68,6 +70,7 @@ QJsonObject WidgetConfig::toJson() const
                     QStringLiteral("main_window_keep_docks_expanding_content_area"),
                     m_mainWindowKeepDocksExpandingContentArea);
     obj[QStringLiteral("new_note_default_file_type")] = m_newNoteDefaultFileType;
+    obj[QStringLiteral("united_entry_expand_all")] = m_unitedEntryExpandAllEnabled;
     return obj;
 }
 
@@ -200,3 +203,13 @@ void WidgetConfig::setNewNoteDefaultFileType(int p_type)
 {
     updateConfig(m_newNoteDefaultFileType, p_type, this);
 }
+
+bool WidgetConfig::getUnitedEntryExpandAllEnabled() const
+{
+    return m_unitedEntryExpandAllEnabled;
+}
+
+void WidgetConfig::setUnitedEntryExpandAllEnabled(bool p_enabled)
+{
+    updateConfig(m_unitedEntryExpandAllEnabled, p_enabled, this);
+}

+ 5 - 0
src/core/widgetconfig.h

@@ -57,6 +57,9 @@ namespace vnotex
         int getNewNoteDefaultFileType() const;
         void setNewNoteDefaultFileType(int p_type);
 
+        bool getUnitedEntryExpandAllEnabled() const;
+        void setUnitedEntryExpandAllEnabled(bool p_enabled);
+
     private:
         int m_outlineAutoExpandedLevel = 6;
 
@@ -85,6 +88,8 @@ namespace vnotex
         bool m_tagExplorerTwoColumnsEnabled = false;
 
         int m_newNoteDefaultFileType = 0;
+
+        bool m_unitedEntryExpandAllEnabled = false;
     };
 }
 

+ 2 - 0
src/data/core/core.qrc

@@ -9,6 +9,7 @@
         <file>icons/new_notebook.svg</file>
         <file>icons/notebook_menu.svg</file>
         <file>icons/task_menu.svg</file>
+        <file>icons/united_entry.svg</file>
         <file>icons/advanced_settings.svg</file>
         <file>icons/new_notebook_from_folder.svg</file>
         <file>icons/discard_editor.svg</file>
@@ -36,6 +37,7 @@
         <file>icons/notebook_default.svg</file>
         <file>icons/file_node.svg</file>
         <file>icons/folder_node.svg</file>
+        <file>icons/other_item.svg</file>
         <file>icons/manage_notebooks.svg</file>
         <file>icons/up_level.svg</file>
         <file>icons/properties.svg</file>

+ 2 - 0
src/data/core/icons/other_item.svg

@@ -0,0 +1,2 @@
+<?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="1648451502935" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9398" width="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
+</style></defs><path d="M692.071703 329.86173c-48.372707-48.387034-112.705921-75.023694-181.146687-75.023694-68.440765 0-132.747373 26.63666-181.148733 75.023694-48.400337 48.374754-75.022671 112.707968-75.022671 181.123151s26.623357 132.74635 75.022671 181.146687c48.374754 48.402383 112.707968 75.023694 181.148733 75.023694 68.441788 0 132.745327-26.647917 181.146687-75.023694 48.427966-48.373731 75.024717-112.706945 75.024717-181.146687C767.096421 442.544115 740.447481 378.237507 692.071703 329.86173z" p-id="9399" fill="#000000"></path></svg>

+ 1 - 0
src/data/core/icons/united_entry.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="1646971542356" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10326" width="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M960 447.968c0-211.744-200.96-384-448-384s-448 172.256-448 384c0 116.48 63.008 226.048 172.896 300.672 14.656 9.984 34.528 6.144 44.448-8.512 9.952-14.624 6.112-34.528-8.512-44.448C180.8 633.184 128 542.912 128 447.968c0-176.448 172.256-320 384-320s384 143.552 384 320-172.256 320-384 320c-1.984 0-3.68 0.768-5.536 1.12-15.168-2.688-30.528 5.184-35.808 20.224-6.112 17.344-46.368 46.624-94.112 73.76 17.472-58.208 9.088-70.688 3.52-78.976a36.034 36.034 0 0 0-29.92-15.936c-17.664 0-32 14.304-32 32 0 5.824 1.536 11.264 4.256 15.936-3.232 18.24-17.216 60.864-33.088 99.872-4.928 12.096-1.984 25.984 7.36 35.072a32.049 32.049 0 0 0 22.272 8.992c4.384 0 8.8-0.896 12.992-2.752 36.48-16.256 147.616-69.152 187.584-125.632C763.072 828.16 960 657.536 960 447.968z" p-id="10327" fill="#000000"></path><path d="M726.624 273.696c-12.512-12.512-32.736-12.512-45.248 0L545.024 410.048c-19.936-12.288-42.784-19.2-66.848-19.2-34.208 0-66.336 13.312-90.496 37.472-49.888 49.888-49.888 131.104 0 181.056 24.16 24.16 56.32 37.472 90.496 37.472 34.208 0 66.336-13.312 90.496-37.472 24.192-24.192 37.536-56.352 37.504-90.592-0.032-22.336-6.24-43.552-16.928-62.496L641.536 404l39.84 39.84c6.24 6.24 14.432 9.376 22.624 9.376s16.384-3.136 22.624-9.376c12.512-12.512 12.512-32.736 0-45.248l-39.84-39.84 39.84-39.84c12.512-12.48 12.512-32.736 0-45.216z m-203.2 290.432c-24.16 24.096-66.368 24.128-90.496 0.032-24.96-25.024-24.96-65.632 0-90.592 12.064-12.064 28.128-18.72 45.248-18.72 17.12 0 33.184 6.656 45.248 18.72 12.096 12.096 18.752 28.16 18.752 45.28s-6.624 33.184-18.752 45.28z" p-id="10328" fill="#000000"></path></svg>

+ 70 - 4
src/data/core/vnotex.json

@@ -59,7 +59,8 @@
             "MoveOneSplitDown" : "Ctrl+G, Shift+J",
             "MoveOneSplitUp" : "Ctrl+G, Shift+K",
             "MoveOneSplitRight" : "Ctrl+G, Shift+L",
-            "OpenLastClosedFile" : "Ctrl+Shift+T"
+            "OpenLastClosedFile" : "Ctrl+Shift+T",
+            "UnitedEntry" : "Ctrl+G, G"
         },
         "file_type_suffixes" : [
             {
@@ -98,7 +99,71 @@
         "history_max_count" : 100,
         "per_notebook_history" : false,
         "//comment" : "Line ending policy for config files, platform/lf/crlf/cr",
-        "line_ending" : "lf"
+        "line_ending" : "lf",
+        "united_entry" : {
+            "alias" : [
+                {
+                    "name" : "q",
+                    "description" : "Search for folders/files by name in all notebooks",
+                    "value" : "find --scope all_notebook --object name --target file --target folder"
+                },
+                {
+                    "name" : "a",
+                    "description" : "Search for files by content in all notebooks",
+                    "value" : "find --scope all_notebook --object content --target file"
+                },
+                {
+                    "name" : "z",
+                    "description" : "Search for files by tag in all notebooks",
+                    "value" : "find --scope all_notebook --object tag --target file"
+                },
+                {
+                    "name" : "w",
+                    "description" : "Search for notebooks by name in all notebooks",
+                    "value" : "find --scope all_notebook --object name --target notebook"
+                },
+                {
+                    "name" : "e",
+                    "description" : "Search for folders/files by name in current notebook",
+                    "value" : "find --scope notebook --object name --target file --target folder"
+                },
+                {
+                    "name" : "d",
+                    "description" : "Search for files by content in current notebook",
+                    "value" : "find --scope notebook --object content --target file"
+                },
+                {
+                    "name" : "c",
+                    "description" : "Search for files by tag in current notebook",
+                    "value" : "find --scope notebook --object tag --target file"
+                },
+                {
+                    "name" : "r",
+                    "description" : "Search for folders/files by name in current folder",
+                    "value" : "find --scope folder --object name --target file --target folder"
+                },
+                {
+                    "name" : "f",
+                    "description" : "Search for files by content in current folder",
+                    "value" : "find --scope folder --object content --target file"
+                },
+                {
+                    "name" : "v",
+                    "description" : "Search for files by tag in current folder",
+                    "value" : "find --scope folder --object tag --target file"
+                },
+                {
+                    "name" : "t",
+                    "description" : "Search for files by name in buffers",
+                    "value" : "find --scope buffer --object name --target file"
+                },
+                {
+                    "name" : "g",
+                    "description" : "Search for files by content in buffers",
+                    "value" : "find --scope buffer --object content --target file"
+                }
+            ]
+        }
     },
     "editor" : {
         "core": {
@@ -131,7 +196,7 @@
                 "TypeMath" : "Ctrl+.",
                 "TypeMathBlock" : "Ctrl+G, .",
                 "TypeQuote" : "",
-                "TypeLink" : "",
+                "TypeLink" : "Ctrl+,",
                 "TypeImage" : "",
                 "TypeTable" : "Ctrl+/",
                 "TypeMark" : "Ctrl+G, M",
@@ -418,6 +483,7 @@
         "main_window_keep_docks_expanding_content_area": ["OutlineDock.vnotex"],
         "snippet_panel_builtin_snippets_visible" : true,
         "tag_explorer_two_columns_enabled" : true,
-        "new_note_default_file_type" : 0
+        "new_note_default_file_type" : 0,
+        "united_entry_expand_all" : false
     }
 }

+ 6 - 2
src/data/extra/themes/moonlight/interface.qss

@@ -469,12 +469,12 @@ QLineEdit {
 }
 
 QLineEdit:focus {
-    border: 2px solid @widgets#qlineedit#focus#border;
+    border: 1px solid @widgets#qlineedit#focus#border;
     background-color: @widgets#qlineedit#focus#bg;
 }
 
 QLineEdit:hover {
-    border: 2px solid @widgets#qlineedit#hover#border;
+    border: 1px solid @widgets#qlineedit#hover#border;
     background-color: @widgets#qlineedit#hover#bg;
 }
 
@@ -1174,3 +1174,7 @@ vnotex--ViewSplit QTabBar[ViewSplitFlash="true"]::tab:selected {
 vte--VTextEdit {
     border: none;
 }
+
+vnotex--EntryPopup {
+    border: 1px solid @widgets#unitedentry#popup#border;
+}

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

@@ -647,6 +647,17 @@
                 "fg" : "@base#master#fg",
                 "bg" : "@base#master#bg"
             }
+        },
+        "unitedentry" : {
+            "icon" : {
+                "fg" : "@base#icon#fg",
+                "busy" : {
+                    "fg" : "@base#master#bg"
+                }
+            },
+            "popup" : {
+                "border" : "@base#normal#border"
+            }
         }
     }
 }

+ 4 - 0
src/data/extra/themes/native/interface.qss

@@ -127,3 +127,7 @@ vnotex--MainWindow QLabel#MainWindowTipsLabel {
 vnotex--ViewSplit QTabBar[ViewSplitFlash="true"]::tab:selected {
     background-color: @widgets#viewsplit#flash#bg;
 }
+
+vnotex--EntryPopup {
+    border: 1px solid @widgets#unitedentry#popup#border;
+}

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

@@ -153,6 +153,17 @@
                     }
                 }
             }
+        },
+        "unitedentry" : {
+            "icon" : {
+                "fg" : "@base#icon#fg",
+                "busy" : {
+                    "fg" : "@base#info#fg"
+                }
+            },
+            "popup" : {
+                "border" : "@base#normal#border"
+            }
         }
     }
 }

+ 6 - 2
src/data/extra/themes/pure/interface.qss

@@ -469,12 +469,12 @@ QLineEdit {
 }
 
 QLineEdit:focus {
-    border: 2px solid @widgets#qlineedit#focus#border;
+    border: 1px solid @widgets#qlineedit#focus#border;
     background-color: @widgets#qlineedit#focus#bg;
 }
 
 QLineEdit:hover {
-    border: 2px solid @widgets#qlineedit#hover#border;
+    border: 1px solid @widgets#qlineedit#hover#border;
     background-color: @widgets#qlineedit#hover#bg;
 }
 
@@ -1174,3 +1174,7 @@ vnotex--ViewSplit QTabBar[ViewSplitFlash="true"]::tab:selected {
 vte--VTextEdit {
     border: none;
 }
+
+vnotex--EntryPopup {
+    border: 1px solid @widgets#unitedentry#popup#border;
+}

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

@@ -643,6 +643,17 @@
                 "fg" : "@base#master#fg",
                 "bg" : "@base#master#bg"
             }
+        },
+        "unitedentry" : {
+            "icon" : {
+                "fg" : "@base#icon#fg",
+                "busy" : {
+                    "fg" : "@base#master#bg"
+                }
+            },
+            "popup" : {
+                "border" : "@base#normal#border"
+            }
         }
     }
 }

+ 6 - 2
src/data/extra/themes/solarized-dark/interface.qss

@@ -465,12 +465,12 @@ QLineEdit {
 }
 
 QLineEdit:focus {
-    border: 2px solid @widgets#qlineedit#focus#border;
+    border: 1px solid @widgets#qlineedit#focus#border;
     background-color: @widgets#qlineedit#focus#bg;
 }
 
 QLineEdit:hover {
-    border: 2px solid @widgets#qlineedit#hover#border;
+    border: 1px solid @widgets#qlineedit#hover#border;
     background-color: @widgets#qlineedit#hover#bg;
 }
 
@@ -1170,3 +1170,7 @@ vnotex--ViewSplit QTabBar[ViewSplitFlash="true"]::tab:selected {
 vte--VTextEdit {
     border: none;
 }
+
+vnotex--EntryPopup {
+    border: 1px solid @widgets#unitedentry#popup#border;
+}

+ 11 - 0
src/data/extra/themes/solarized-dark/palette.json

@@ -648,6 +648,17 @@
                 "fg" : "@base#master#fg",
                 "bg" : "@base#master#bg"
             }
+        },
+        "unitedentry" : {
+            "icon" : {
+                "fg" : "@base#icon#fg",
+                "busy" : {
+                    "fg" : "@base#master#bg"
+                }
+            },
+            "popup" : {
+                "border" : "@base#normal#border"
+            }
         }
     }
 }

+ 6 - 2
src/data/extra/themes/solarized-light/interface.qss

@@ -465,12 +465,12 @@ QLineEdit {
 }
 
 QLineEdit:focus {
-    border: 2px solid @widgets#qlineedit#focus#border;
+    border: 1px solid @widgets#qlineedit#focus#border;
     background-color: @widgets#qlineedit#focus#bg;
 }
 
 QLineEdit:hover {
-    border: 2px solid @widgets#qlineedit#hover#border;
+    border: 1px solid @widgets#qlineedit#hover#border;
     background-color: @widgets#qlineedit#hover#bg;
 }
 
@@ -1170,3 +1170,7 @@ vnotex--ViewSplit QTabBar[ViewSplitFlash="true"]::tab:selected {
 vte--VTextEdit {
     border: none;
 }
+
+vnotex--EntryPopup {
+    border: 1px solid @widgets#unitedentry#popup#border;
+}

+ 11 - 0
src/data/extra/themes/solarized-light/palette.json

@@ -648,6 +648,17 @@
                 "fg" : "@base#master#fg",
                 "bg" : "@base#master#bg"
             }
+        },
+        "unitedentry" : {
+            "icon" : {
+                "fg" : "@base#icon#fg",
+                "busy" : {
+                    "fg" : "@base#master#bg"
+                }
+            },
+            "popup" : {
+                "border" : "@base#normal#border"
+            }
         }
     }
 }

+ 6 - 2
src/data/extra/themes/vscode-dark/interface.qss

@@ -465,12 +465,12 @@ QLineEdit {
 }
 
 QLineEdit:focus {
-    border: 2px solid @widgets#qlineedit#focus#border;
+    border: 1px solid @widgets#qlineedit#focus#border;
     background-color: @widgets#qlineedit#focus#bg;
 }
 
 QLineEdit:hover {
-    border: 2px solid @widgets#qlineedit#hover#border;
+    border: 1px solid @widgets#qlineedit#hover#border;
     background-color: @widgets#qlineedit#hover#bg;
 }
 
@@ -1170,3 +1170,7 @@ vnotex--ViewSplit QTabBar[ViewSplitFlash="true"]::tab:selected {
 vte--VTextEdit {
     border: none;
 }
+
+vnotex--EntryPopup {
+    border: 1px solid @widgets#unitedentry#popup#border;
+}

+ 11 - 0
src/data/extra/themes/vscode-dark/palette.json

@@ -647,6 +647,17 @@
                 "fg" : "@base#master#fg",
                 "bg" : "@base#master#bg"
             }
+        },
+        "unitedentry" : {
+            "icon" : {
+                "fg" : "@base#icon#fg",
+                "busy" : {
+                    "fg" : "@base#master#bg"
+                }
+            },
+            "popup" : {
+                "border" : "@base#normal#border"
+            }
         }
     }
 }

+ 30 - 0
src/search/isearchinfoprovider.h

@@ -0,0 +1,30 @@
+#ifndef ISEARCHINFOPROVIDER_H
+#define ISEARCHINFOPROVIDER_H
+
+#include <QList>
+#include <QVector>
+
+namespace vnotex
+{
+    class Node;
+    class Notebook;
+    class Buffer;
+
+    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;
+    };
+}
+
+#endif // ISEARCHINFOPROVIDER_H

+ 3 - 0
src/search/search.pri

@@ -3,8 +3,10 @@ QT += widgets
 HEADERS += \
     $$PWD/filesearchengine.h \
     $$PWD/isearchengine.h \
+    $$PWD/isearchinfoprovider.h \
     $$PWD/searchdata.h \
     $$PWD/searcher.h \
+    $$PWD/searchhelper.h \
     $$PWD/searchresultitem.h \
     $$PWD/searchtoken.h
 
@@ -12,6 +14,7 @@ SOURCES += \
     $$PWD/filesearchengine.cpp \
     $$PWD/searchdata.cpp \
     $$PWD/searcher.cpp \
+    $$PWD/searchhelper.cpp \
     $$PWD/searchresultitem.cpp \
     $$PWD/searchtoken.cpp
 

+ 5 - 0
src/search/searchdata.cpp

@@ -45,3 +45,8 @@ bool SearchOption::operator==(const SearchOption &p_other) const
            && m_engine == p_other.m_engine
            && m_findOptions == p_other.m_findOptions;
 }
+
+bool SearchOption::strictEquals(const SearchOption &p_other) const
+{
+    return (*this == p_other) && m_keyword == p_other.m_keyword;
+}

+ 2 - 0
src/search/searchdata.h

@@ -87,6 +87,8 @@ namespace vnotex
 
         bool operator==(const SearchOption &p_other) const;
 
+        bool strictEquals(const SearchOption &p_other) const;
+
         QString m_keyword;
 
         QString m_filePattern;

+ 99 - 0
src/search/searchhelper.cpp

@@ -0,0 +1,99 @@
+#include "searchhelper.h"
+
+#include <search/isearchinfoprovider.h>
+
+using namespace vnotex;
+
+bool SearchHelper::isSearchOptionValid(const SearchOption &p_option, QString &p_msg)
+{
+    if (p_option.m_keyword.isEmpty()) {
+        p_msg = Searcher::tr("Invalid keyword");
+        return false;
+    }
+
+    if (p_option.m_objects == SearchObject::ObjectNone) {
+        p_msg = Searcher::tr("No object specified");
+        return false;
+    }
+
+    if (p_option.m_targets == SearchTarget::TargetNone) {
+        p_msg = Searcher::tr("No target specified");
+        return false;
+    }
+
+    if (p_option.m_findOptions & FindOption::FuzzySearch
+        && p_option.m_objects & SearchObject::SearchContent) {
+        p_msg = Searcher::tr("Fuzzy search is not allowed when searching content");
+        return false;
+    }
+
+    p_msg.clear();
+    return true;
+}
+
+SearchState SearchHelper::searchOnProvider(Searcher *p_searcher,
+                                           const QSharedPointer<SearchOption> &p_option,
+                                           const QSharedPointer<ISearchInfoProvider> &p_provider,
+                                           QString &p_msg)
+{
+    p_msg.clear();
+
+    if (!isSearchOptionValid(*p_option, p_msg)) {
+        return SearchState::Failed;
+    }
+
+    SearchState state = SearchState::Finished;
+
+    switch (p_option->m_scope) {
+    case SearchScope::Buffers:
+    {
+        auto buffers = p_provider->getBuffers();
+        if (buffers.isEmpty()) {
+            break;
+        }
+        state = p_searcher->search(p_option, buffers);
+        break;
+    }
+
+    case SearchScope::CurrentFolder:
+    {
+        auto notebook = p_provider->getCurrentNotebook();
+        if (!notebook) {
+            break;
+        }
+        auto folder = p_provider->getCurrentFolder();
+        if (!folder) {
+            break;
+        }
+
+        state = p_searcher->search(p_option, folder);
+        break;
+    }
+
+    case SearchScope::CurrentNotebook:
+    {
+        auto notebook = p_provider->getCurrentNotebook();
+        if (!notebook) {
+            break;
+        }
+
+        QVector<Notebook *> notebooks;
+        notebooks.push_back(notebook);
+        state = p_searcher->search(p_option, notebooks);
+        break;
+    }
+
+    case SearchScope::AllNotebooks:
+    {
+        auto notebooks = p_provider->getNotebooks();
+        if (notebooks.isEmpty()) {
+            break;
+        }
+
+        state = p_searcher->search(p_option, notebooks);
+        break;
+    }
+    }
+
+    return state;
+}

+ 28 - 0
src/search/searchhelper.h

@@ -0,0 +1,28 @@
+#ifndef SEARCHHELPER_H
+#define SEARCHHELPER_H
+
+#include <QSharedPointer>
+
+#include "searchdata.h"
+#include "searcher.h"
+
+namespace vnotex
+{
+    class ISearchInfoProvider;
+
+    class SearchHelper
+    {
+    public:
+        SearchHelper() = delete;
+
+        static SearchState searchOnProvider(Searcher *p_searcher,
+                                            const QSharedPointer<SearchOption> &p_option,
+                                            const QSharedPointer<ISearchInfoProvider> &p_provider,
+                                            QString &p_msg);
+
+    private:
+        static bool isSearchOptionValid(const SearchOption &p_option, QString &p_msg);
+    };
+}
+
+#endif // SEARCHHELPER_H

+ 31 - 24
src/search/searchtoken.cpp

@@ -8,8 +8,6 @@
 
 using namespace vnotex;
 
-QScopedPointer<QCommandLineParser> SearchToken::s_parser;
-
 void SearchToken::clear()
 {
     m_type = Type::PlainText;
@@ -157,31 +155,40 @@ bool SearchToken::isEmpty() const
     return constraintSize() == 0;
 }
 
-void SearchToken::createCommandLineParser()
+QCommandLineParser *SearchToken::getCommandLineParser()
 {
-    if (s_parser) {
-        return;
+    static QScopedPointer<QCommandLineParser> parser;
+
+    if (parser) {
+        return parser.data();
     }
 
-    s_parser.reset(new QCommandLineParser());
-    s_parser->setApplicationDescription(SearchPanel::tr("Full-text search."));
+    parser.reset(new QCommandLineParser());
+    parser->setApplicationDescription(SearchPanel::tr("Full-text search."));
+
+    parser->addPositionalArgument("keywords", SearchPanel::tr("Keywords to search for."));
+
+    addSearchOptionsToCommand(parser.data());
 
+    return parser.data();
+}
+
+void SearchToken::addSearchOptionsToCommand(QCommandLineParser *p_parser)
+{
     QCommandLineOption caseSensitiveOpt(QStringList() << "c" << "case-sensitive", SearchPanel::tr("Search in case sensitive."));
-    s_parser->addOption(caseSensitiveOpt);
+    p_parser->addOption(caseSensitiveOpt);
 
     QCommandLineOption regularExpressionOpt(QStringList() << "r" << "regular-expression", SearchPanel::tr("Search by regular expression."));
-    s_parser->addOption(regularExpressionOpt);
+    p_parser->addOption(regularExpressionOpt);
 
     QCommandLineOption wholeWordOnlyOpt(QStringList() << "w" << "whole-word-only", SearchPanel::tr("Search whole word only."));
-    s_parser->addOption(wholeWordOnlyOpt);
+    p_parser->addOption(wholeWordOnlyOpt);
 
     QCommandLineOption fuzzySearchOpt(QStringList() << "f" << "fuzzy-search", SearchPanel::tr("Do a fuzzy search (not applicable to content search)."));
-    s_parser->addOption(fuzzySearchOpt);
+    p_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."));
+    p_parser->addOption(orOpt);
 }
 
 bool SearchToken::compile(const QString &p_keyword, FindOptions p_options, SearchToken &p_token)
@@ -193,7 +200,7 @@ bool SearchToken::compile(const QString &p_keyword, FindOptions p_options, Searc
         return false;
     }
 
-    createCommandLineParser();
+    auto parser = getCommandLineParser();
 
     auto caseSensitivity = p_options & FindOption::CaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive;
     bool isRegularExpression = p_options & FindOption::RegularExpression;
@@ -203,25 +210,25 @@ bool SearchToken::compile(const QString &p_keyword, FindOptions p_options, Searc
     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))
+    if (!parser->parse(args))
     {
         return false;
     }
 
-    if (s_parser->isSet("c")) {
+    if (parser->isSet("c")) {
         caseSensitivity = Qt::CaseSensitive;
     }
-    if (s_parser->isSet("r")) {
+    if (parser->isSet("r")) {
         isRegularExpression = true;
     }
-    if (s_parser->isSet("w")) {
+    if (parser->isSet("w")) {
         isWholeWordOnly = true;
     }
-    if (s_parser->isSet("f")) {
+    if (parser->isSet("f")) {
         isFuzzySearch = true;
     }
 
-    args = s_parser->positionalArguments();
+    args = parser->positionalArguments();
     if (args.isEmpty()) {
         return false;
     }
@@ -232,7 +239,7 @@ bool SearchToken::compile(const QString &p_keyword, FindOptions p_options, Searc
     } else {
         p_token.m_type = Type::PlainText;
     }
-    p_token.m_operator = s_parser->isSet("o") ? Operator::Or : Operator::And;
+    p_token.m_operator = parser->isSet("o") ? Operator::Or : Operator::And;
 
     auto patternOptions = caseSensitivity == Qt::CaseInsensitive ? QRegularExpression::CaseInsensitiveOption
                                                                  : QRegularExpression::NoPatternOption;
@@ -266,8 +273,8 @@ bool SearchToken::compile(const QString &p_keyword, FindOptions p_options, Searc
 
 QString SearchToken::getHelpText()
 {
-    createCommandLineParser();
-    auto text = s_parser->helpText();
+    auto parser = getCommandLineParser();
+    auto text = parser->helpText();
     // Skip the first line containing the application name.
     return text.mid(text.indexOf('\n') + 1);
 }

+ 3 - 3
src/search/searchtoken.h

@@ -62,8 +62,10 @@ namespace vnotex
 
         static QString getHelpText();
 
+        static void addSearchOptionsToCommand(QCommandLineParser *p_parser);
+
     private:
-        static void createCommandLineParser();
+        static QCommandLineParser *getCommandLineParser();
 
         Type m_type = Type::PlainText;
 
@@ -79,8 +81,6 @@ namespace vnotex
         QBitArray m_matchedConstraintsInBatchMode;
 
         int m_matchedConstraintsCountInBatchMode = 0;
-
-        static QScopedPointer<QCommandLineParser> s_parser;
     };
 }
 

+ 2 - 0
src/src.pro

@@ -62,6 +62,8 @@ include($$PWD/core/core.pri)
 
 include($$PWD/widgets/widgets.pri)
 
+include($$PWD/unitedentry/unitedentry.pri)
+
 RESOURCES += \
     $$PWD/data/core/core.qrc
 

+ 86 - 0
src/unitedentry/entrypopup.cpp

@@ -0,0 +1,86 @@
+#include "entrypopup.h"
+
+#include <QVBoxLayout>
+#include <QDebug>
+#include <QWindow>
+
+using namespace vnotex;
+
+EntryPopup::EntryPopup(QWidget *p_parent)
+    : QFrame(p_parent)
+{
+    Q_ASSERT(p_parent);
+    auto layout = new QVBoxLayout(this);
+    Q_UNUSED(layout);
+
+    setWindowFlags(Qt::ToolTip);
+    setFocusPolicy(Qt::FocusPolicy::ClickFocus);
+}
+
+EntryPopup::~EntryPopup()
+{
+    if (m_widget) {
+        takeWidget(m_widget.data());
+    }
+}
+
+void EntryPopup::setWidget(const QSharedPointer<QWidget> &p_widget)
+{
+    Q_ASSERT(p_widget);
+
+    if (p_widget == m_widget) {
+        return;
+    }
+
+    if (m_widget) {
+        takeWidget(m_widget.data());
+    }
+
+    layout()->addWidget(p_widget.data());
+    m_widget = p_widget;
+    m_widget->show();
+
+    updateGeometryToContents();
+}
+
+void EntryPopup::takeWidget(QWidget *p_widget)
+{
+    layout()->removeWidget(p_widget);
+    p_widget->hide();
+    p_widget->setParent(nullptr);
+}
+
+void EntryPopup::showEvent(QShowEvent *p_event)
+{
+    QFrame::showEvent(p_event);
+
+    updateGeometryToContents();
+}
+
+void EntryPopup::updateGeometryToContents()
+{
+    adjustSize();
+
+    auto pa = parentWidget();
+    auto pos = pa->mapToGlobal(QPoint(0, pa->height()));
+    setGeometry(QRect(pos, preferredSize()));
+
+    if (m_widget) {
+        m_widget->updateGeometry();
+    }
+}
+
+QSize EntryPopup::preferredSize() const
+{
+    const int minWidth = 400;
+    const int minHeight = 300;
+
+    auto pa = parentWidget();
+    int w = pa->width();
+    int h = sizeHint().height();
+    if (auto win = pa->window()) {
+        w = qMax(w, qMin(win->width() - 500, 900));
+        h = qMax(h, qMin(win->height() - 500, 800));
+    }
+    return QSize(qMax(minWidth, w), qMax(h, minHeight));
+}

+ 34 - 0
src/unitedentry/entrypopup.h

@@ -0,0 +1,34 @@
+#ifndef ENTRYPOPUP_H
+#define ENTRYPOPUP_H
+
+#include <QFrame>
+#include <QSharedPointer>
+
+namespace vnotex
+{
+    class EntryPopup : public QFrame
+    {
+        Q_OBJECT
+    public:
+        explicit EntryPopup(QWidget *p_parent = nullptr);
+
+        ~EntryPopup();
+
+        void setWidget(const QSharedPointer<QWidget> &p_widget);
+
+        void updateGeometryToContents();
+
+    protected:
+        void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE;
+
+    private:
+        QSize preferredSize() const;
+
+        void takeWidget(QWidget *p_widget);
+
+    private:
+        QSharedPointer<QWidget> m_widget = nullptr;
+    };
+}
+
+#endif // ENTRYPOPUP_H

+ 29 - 0
src/unitedentry/entrywidgetfactory.cpp

@@ -0,0 +1,29 @@
+#include "entrywidgetfactory.h"
+
+#include <QLabel>
+
+#include <widgets/widgetsfactory.h>
+#include <widgets/treewidget.h>
+
+using namespace vnotex;
+
+QSharedPointer<QTreeWidget> EntryWidgetFactory::createTreeWidget(int p_columnCount)
+{
+    auto tree = QSharedPointer<TreeWidget>::create(TreeWidget::Flag::EnhancedStyle, nullptr);
+    tree->setColumnCount(p_columnCount);
+    tree->setHeaderHidden(true);
+    TreeWidget::showHorizontalScrollbar(tree.data());
+    return tree;
+}
+
+QSharedPointer<QLabel> EntryWidgetFactory::createLabel(const QString &p_info)
+{
+    auto label = QSharedPointer<QLabel>::create(p_info, nullptr);
+    label->setAlignment(Qt::AlignTop | Qt::AlignLeft);
+
+    auto fnt = label->font();
+    fnt.setPointSize(fnt.pointSize() + 2);
+    label->setFont(fnt);
+
+    return label;
+}

+ 23 - 0
src/unitedentry/entrywidgetfactory.h

@@ -0,0 +1,23 @@
+#ifndef ENTRYWIDGETFACTORY_H
+#define ENTRYWIDGETFACTORY_H
+
+#include <QSharedPointer>
+
+class QTreeWidget;
+class QLabel;
+class QString;
+
+namespace vnotex
+{
+    class EntryWidgetFactory
+    {
+    public:
+        EntryWidgetFactory() = delete;
+
+        static QSharedPointer<QTreeWidget> createTreeWidget(int p_columnCount);
+
+        static QSharedPointer<QLabel> createLabel(const QString &p_info);
+    };
+}
+
+#endif // ENTRYWIDGETFACTORY_H

+ 346 - 0
src/unitedentry/findunitedentry.cpp

@@ -0,0 +1,346 @@
+#include "findunitedentry.h"
+
+#include <QLabel>
+#include <QCommandLineOption>
+#include <QTimer>
+#include <QDebug>
+
+#include <utils/processutils.h>
+#include <search/searchtoken.h>
+#include <search/searchdata.h>
+#include <search/isearchinfoprovider.h>
+#include <search/searchresultitem.h>
+#include <search/searcher.h>
+#include <search/searchhelper.h>
+#include <search/searchtoken.h>
+#include <core/fileopenparameters.h>
+#include <core/vnotex.h>
+#include <widgets/treewidget.h>
+#include "unitedentryhelper.h"
+#include "entrywidgetfactory.h"
+#include "unitedentrymgr.h"
+
+using namespace vnotex;
+
+FindUnitedEntry::FindUnitedEntry(const QSharedPointer<ISearchInfoProvider> &p_provider,
+                                 const UnitedEntryMgr *p_mgr,
+                                 QObject *p_parent)
+    : IUnitedEntry("find",
+                   tr("Search for files in notebooks"),
+                   p_mgr,
+                   p_parent),
+      m_provider(p_provider)
+{
+    m_processTimer = new QTimer(this);
+    m_processTimer->setSingleShot(true);
+    m_processTimer->setInterval(500);
+    connect(m_processTimer, &QTimer::timeout,
+            this, &FindUnitedEntry::doProcessInternal);
+}
+
+void FindUnitedEntry::initOnFirstProcess()
+{
+    m_parser.setApplicationDescription(tr("Search for files in notebooks with advanced options for scope, object, target and so on."));
+
+    m_parser.addPositionalArgument("keywords", tr("Keywords to search for."));
+
+    QCommandLineOption scopeOpt({"s", "scope"},
+                                tr("Search scope. Possible values: buffer/folder/notebook/all_notebook."),
+                                tr("search_scope"),
+                                "notebook");
+    m_parser.addOption(scopeOpt);
+
+    QCommandLineOption objectOpt({"b", "object"},
+                                 tr("Search objects. Possible values: name/content/tag/path."),
+                                 tr("search_objects"));
+    objectOpt.setDefaultValues({"name", "content"});
+    m_parser.addOption(objectOpt);
+
+    QCommandLineOption targetOpt({"t", "target"},
+                                 tr("Search targets. Possible values: file/folder/notebook."),
+                                 tr("search_targets"));
+    targetOpt.setDefaultValues({"file", "folder"});
+    m_parser.addOption(targetOpt);
+
+    QCommandLineOption patternOpt({"p", "pattern"},
+                                 tr("Wildcard pattern of files to search."),
+                                 tr("file_pattern"));
+    m_parser.addOption(patternOpt);
+
+    SearchToken::addSearchOptionsToCommand(&m_parser);
+}
+
+void FindUnitedEntry::processInternal(const QString &p_args,
+                                      const std::function<void(const QSharedPointer<QWidget> &)> &p_popupWidgetFunc)
+{
+    // Do another timer delay here since it is a very expensive operation.
+    m_processTimer->stop();
+
+    Q_ASSERT(!isOngoing());
+
+    setOngoing(true);
+
+    auto args = ProcessUtils::parseCombinedArgString(p_args);
+    // The parser needs the first arg to be the application name.
+    args.prepend(name());
+    bool ret = m_parser.parse(args);
+    const auto positionalArgs = m_parser.positionalArguments();
+    if (!ret) {
+        auto label = EntryWidgetFactory::createLabel(m_parser.errorText());
+        p_popupWidgetFunc(label);
+        finish();
+        return;
+    }
+    if (positionalArgs.isEmpty()) {
+        auto label = EntryWidgetFactory::createLabel(getHelpText());
+        p_popupWidgetFunc(label);
+        finish();
+        return;
+    }
+
+    auto opt = collectOptions();
+    if (m_searchOption && m_searchOption->strictEquals(*opt)) {
+        // Reuse last result.
+        p_popupWidgetFunc(m_resultTree);
+        finish();
+        return;
+    }
+
+    m_searchOption = opt;
+
+    m_searchTokenOfSession.clear();
+
+    prepareResultTree();
+
+    p_popupWidgetFunc(m_resultTree);
+
+    m_processTimer->start();
+}
+
+QSharedPointer<SearchOption> FindUnitedEntry::collectOptions() const
+{
+    auto opt = QSharedPointer<SearchOption>::create();
+
+    opt->m_engine = SearchEngine::Internal;
+
+    opt->m_keyword = m_parser.positionalArguments().join(QLatin1Char(' '));
+    Q_ASSERT(!opt->m_keyword.isEmpty());
+
+    opt->m_filePattern = m_parser.value("p");
+
+    {
+        SearchScope scope = SearchScope::CurrentNotebook;
+        const auto scopeStr = m_parser.value("s");
+        if (scopeStr == QStringLiteral("buffer")) {
+            scope = SearchScope::Buffers;
+        } else if (scopeStr == QStringLiteral("folder")) {
+            scope = SearchScope::CurrentFolder;
+        } else if (scopeStr == QStringLiteral("notebook")) {
+            scope = SearchScope::CurrentNotebook;
+        } else if (scopeStr == QStringLiteral("all_notebook")) {
+            scope = SearchScope::AllNotebooks;
+        }
+        opt->m_scope = scope;
+    }
+
+    {
+        SearchObjects objects = SearchObject::ObjectNone;
+        const auto objectStrs = m_parser.values("b");
+        for (const auto &str : objectStrs) {
+            if (str == QStringLiteral("name")) {
+                objects |= SearchObject::SearchName;
+            } else if (str == QStringLiteral("content")) {
+                objects |= SearchObject::SearchContent;
+            } else if (str == QStringLiteral("tag")) {
+                objects |= SearchObject::SearchTag;
+            } else if (str == QStringLiteral("path")) {
+                objects |= SearchObject::SearchPath;
+            }
+        }
+        opt->m_objects = objects;
+    }
+
+    {
+        SearchTargets targets = SearchTarget::TargetNone;
+        auto targetStrs = m_parser.values("t");
+        for (const auto &str : targetStrs) {
+            if (str == QStringLiteral("file")) {
+                targets |= SearchTarget::SearchFile;
+            } else if (str == QStringLiteral("folder")) {
+                targets |= SearchTarget::SearchFolder;
+            } else if (str == QStringLiteral("notebook")) {
+                targets |= SearchTarget::SearchNotebook;
+            }
+        }
+        opt->m_targets = targets;
+    }
+
+    {
+        FindOptions options = FindOption::FindNone;
+        if (m_parser.isSet("c")) {
+            options |= FindOption::CaseSensitive;
+        }
+        if (m_parser.isSet("r")) {
+            options |= FindOption::RegularExpression;
+        }
+        if (m_parser.isSet("w")) {
+            options |= FindOption::WholeWordOnly;
+        }
+        if (m_parser.isSet("f")) {
+            options |= FindOption::FuzzySearch;
+        }
+        opt->m_findOptions = options;
+    }
+
+    return opt;
+}
+
+QString FindUnitedEntry::getHelpText() const
+{
+    auto text = m_parser.helpText();
+    // Skip the first line containing the application name.
+    return text.mid(text.indexOf('\n') + 1);
+}
+
+Searcher *FindUnitedEntry::getSearcher()
+{
+    if (!m_searcher) {
+        m_searcher = new Searcher(this);
+        connect(m_searcher, &Searcher::resultItemAdded,
+                this, [this](const QSharedPointer<SearchResultItem> &p_item) {
+                    addLocation(p_item->m_location);
+                });
+        connect(m_searcher, &Searcher::resultItemsAdded,
+                this, [this](const QVector<QSharedPointer<SearchResultItem>> &p_items) {
+                    for (const auto &item : p_items) {
+                        addLocation(item->m_location);
+                    }
+                });
+        connect(m_searcher, &Searcher::finished,
+                this, &FindUnitedEntry::handleSearchFinished);
+    }
+    return m_searcher;
+}
+
+void FindUnitedEntry::handleSearchFinished(SearchState p_state)
+{
+    if (p_state != SearchState::Busy) {
+        getSearcher()->clear();
+        finish();
+    }
+}
+
+void FindUnitedEntry::prepareResultTree()
+{
+    if (!m_resultTree) {
+        m_resultTree = EntryWidgetFactory::createTreeWidget(1);
+        connect(m_resultTree.data(), &QTreeWidget::itemActivated,
+                this, &FindUnitedEntry::handleItemActivated);
+    }
+
+    m_resultTree->clear();
+}
+
+void FindUnitedEntry::addLocation(const ComplexLocation &p_location)
+{
+    auto item = new QTreeWidgetItem(m_resultTree.data());
+    item->setText(0, p_location.m_displayPath);
+    item->setIcon(0, UnitedEntryHelper::itemIcon(UnitedEntryHelper::locationTypeToItemType(p_location.m_type)));
+    item->setData(0, Qt::UserRole, p_location.m_path);
+    item->setToolTip(0, p_location.m_path);
+
+    // Add sub items.
+    for (const auto &line : p_location.m_lines) {
+        auto subItem = new QTreeWidgetItem(item);
+
+        // Truncate the text.
+        if (line.m_text.size() > 500) {
+            subItem->setText(0, line.m_text.left(500));
+        } else {
+            subItem->setText(0, line.m_text);
+        }
+
+        if (!line.m_segments.isEmpty()) {
+            subItem->setData(0, HighlightsRole, QVariant::fromValue(line.m_segments));
+        }
+
+        subItem->setData(0, Qt::UserRole, line.m_lineNumber);
+    }
+
+    if (m_mgr->getExpandAllEnabled()) {
+        item->setExpanded(true);
+    }
+
+    if (m_resultTree->topLevelItemCount() == 1) {
+        m_resultTree->setCurrentItem(item);
+    }
+}
+
+void FindUnitedEntry::doProcessInternal()
+{
+    if (isAskedToStop()) {
+        finish();
+        return;
+    }
+
+    QString msg;
+    auto state = SearchHelper::searchOnProvider(getSearcher(), m_searchOption, m_provider, msg);
+    if (!msg.isEmpty()) {
+        qWarning() << msg;
+    }
+
+    handleSearchFinished(state);
+}
+
+void FindUnitedEntry::stop()
+{
+    IUnitedEntry::stop();
+
+    if (m_processTimer->isActive()) {
+        m_processTimer->stop();
+        // Let it go finished.
+        doProcessInternal();
+    }
+}
+
+void FindUnitedEntry::finish()
+{
+    setOngoing(false);
+    emit finished();
+}
+
+QSharedPointer<QWidget> FindUnitedEntry::currentPopupWidget() const
+{
+    return m_resultTree;
+}
+
+void FindUnitedEntry::handleItemActivated(QTreeWidgetItem *p_item, int p_column)
+{
+    Q_UNUSED(p_column);
+
+    if (!m_searchTokenOfSession) {
+        if (m_searchOption->m_objects & SearchObject::SearchContent) {
+            m_searchTokenOfSession = QSharedPointer<SearchToken>::create(getSearcher()->getToken());
+        }
+    }
+
+    // TODO: decode the path of location and handle different types of destination.
+    auto paras = QSharedPointer<FileOpenParameters>::create();
+
+    QString itemPath;
+    auto pa = p_item->parent();
+    if (pa) {
+        itemPath = pa->data(0, Qt::UserRole).toString();
+        paras->m_lineNumber = p_item->data(0, Qt::UserRole).toInt();
+    } else {
+        itemPath = p_item->data(0, Qt::UserRole).toString();
+        // Use the first line number if there is any.
+        if (p_item->childCount() > 0) {
+            auto childItem = p_item->child(0);
+            paras->m_lineNumber = childItem->data(0, Qt::UserRole).toInt();
+        }
+    }
+
+    paras->m_searchToken = m_searchTokenOfSession;
+    emit VNoteX::getInst().openFileRequested(itemPath, paras);
+}

+ 75 - 0
src/unitedentry/findunitedentry.h

@@ -0,0 +1,75 @@
+#ifndef FINDUNITEDENTRY_H
+#define FINDUNITEDENTRY_H
+
+#include "iunitedentry.h"
+
+#include <QCommandLineParser>
+#include <QSharedPointer>
+
+#include <search/searchdata.h>
+
+class QTreeWidget;
+class QTreeWidgetItem;
+class QTimer;
+
+namespace vnotex
+{
+    class Searcher;
+    class ISearchInfoProvider;
+    struct ComplexLocation;
+    class SearchToken;
+
+    class FindUnitedEntry : public IUnitedEntry
+    {
+        Q_OBJECT
+    public:
+        FindUnitedEntry(const QSharedPointer<ISearchInfoProvider> &p_provider,
+                        const UnitedEntryMgr *p_mgr,
+                        QObject *p_parent = nullptr);
+
+        void stop() Q_DECL_OVERRIDE;
+
+        QSharedPointer<QWidget> currentPopupWidget() const Q_DECL_OVERRIDE;
+
+    protected:
+        void initOnFirstProcess() Q_DECL_OVERRIDE;
+
+        void processInternal(const QString &p_args,
+                             const std::function<void(const QSharedPointer<QWidget> &)> &p_popupWidgetFunc) Q_DECL_OVERRIDE;
+
+    private:
+        QString getHelpText() const;
+
+        QSharedPointer<SearchOption> collectOptions() const;
+
+        Searcher *getSearcher();
+
+        void handleSearchFinished(SearchState p_state);
+
+        void prepareResultTree();
+
+        void addLocation(const ComplexLocation &p_location);
+
+        void doProcessInternal();
+
+        void finish();
+
+        void handleItemActivated(QTreeWidgetItem *p_item, int p_column);
+
+        QSharedPointer<ISearchInfoProvider> m_provider;
+
+        QCommandLineParser m_parser;
+
+        Searcher *m_searcher = nullptr;
+
+        QSharedPointer<QTreeWidget> m_resultTree;
+
+        QTimer *m_processTimer = nullptr;
+
+        QSharedPointer<SearchOption> m_searchOption;
+
+        QSharedPointer<SearchToken> m_searchTokenOfSession;
+    };
+}
+
+#endif // FINDUNITEDENTRY_H

+ 48 - 0
src/unitedentry/helpunitedentry.cpp

@@ -0,0 +1,48 @@
+#include "helpunitedentry.h"
+
+#include <widgets/treewidget.h>
+
+#include "entrywidgetfactory.h"
+
+using namespace vnotex;
+
+HelpUnitedEntry::HelpUnitedEntry(const UnitedEntryMgr *p_mgr, QObject *p_parent)
+    : IUnitedEntry("help",
+                   tr("Help information about United Entry"),
+                   p_mgr,
+                   p_parent)
+{
+}
+
+QSharedPointer<QWidget> HelpUnitedEntry::currentPopupWidget() const
+{
+    return m_infoTree;
+}
+
+void HelpUnitedEntry::initOnFirstProcess()
+{
+    m_infoTree = EntryWidgetFactory::createTreeWidget(2);
+    m_infoTree->setHeaderHidden(false);
+    m_infoTree->setHeaderLabels(QStringList() << tr("Shortcut") << tr("Description"));
+
+    QVector<QStringList> shortcuts = {{"Esc/Ctrl+[", tr("Close United Entry")},
+                                      {"Up/Ctrl+K", tr("Go to previous item")},
+                                      {"Down/Ctrl+J", tr("Go to next item")},
+                                      {"Ctrl+L", tr("Go to the item one level up")},
+                                      {"Ctrl+I", tr("Expand/Collapse current item")},
+                                      {"Ctrl+B", tr("Expand/Collapse all the items")},
+                                      {"Enter", tr("Activate current item")},
+                                      {"Ctrl+E", tr("Clear the input except the entry name")},
+                                      {"Ctrl+F", tr("Select the entry name")},
+                                      {"Ctrl+D", tr("Stop current entry")}};
+    for (const auto &shortcut : shortcuts) {
+        m_infoTree->addTopLevelItem(new QTreeWidgetItem(shortcut));
+    }
+}
+
+void HelpUnitedEntry::processInternal(const QString &p_args,
+                                      const std::function<void(const QSharedPointer<QWidget> &)> &p_popupWidgetFunc)
+{
+    Q_UNUSED(p_args);
+    p_popupWidgetFunc(m_infoTree);
+}

+ 31 - 0
src/unitedentry/helpunitedentry.h

@@ -0,0 +1,31 @@
+#ifndef HELPUNITEDENTRY_H
+#define HELPUNITEDENTRY_H
+
+#include "iunitedentry.h"
+
+#include <QSharedPointer>
+
+class QTreeWidget;
+
+namespace vnotex
+{
+    class HelpUnitedEntry : public IUnitedEntry
+    {
+        Q_OBJECT
+    public:
+        HelpUnitedEntry(const UnitedEntryMgr *p_mgr, QObject *p_parent = nullptr);
+
+        QSharedPointer<QWidget> currentPopupWidget() const Q_DECL_OVERRIDE;
+
+    protected:
+        void initOnFirstProcess() Q_DECL_OVERRIDE;
+
+        void processInternal(const QString &p_args,
+                             const std::function<void(const QSharedPointer<QWidget> &)> &p_popupWidgetFunc) Q_DECL_OVERRIDE;
+
+    private:
+        QSharedPointer<QTreeWidget> m_infoTree;
+    };
+}
+
+#endif // HELPUNITEDENTRY_H

+ 138 - 0
src/unitedentry/iunitedentry.cpp

@@ -0,0 +1,138 @@
+#include "iunitedentry.h"
+
+#include <QWidget>
+#include <QCoreApplication>
+#include <QKeyEvent>
+
+#include <widgets/treewidget.h>
+
+using namespace vnotex;
+
+IUnitedEntry::IUnitedEntry(const QString &p_name,
+                           const QString &p_description,
+                           const UnitedEntryMgr *p_mgr,
+                           QObject *p_parent)
+    : QObject(p_parent),
+      m_name(p_name),
+      m_description(p_description),
+      m_mgr(p_mgr)
+{
+}
+
+const QString &IUnitedEntry::name() const
+{
+    return m_name;
+}
+
+QString IUnitedEntry::description() const
+{
+    return m_description;
+}
+
+void IUnitedEntry::process(const QString &p_args,
+                           const std::function<void(const QSharedPointer<QWidget> &)> &p_popupWidgetFunc)
+{
+    if (!m_initialized) {
+        m_initialized = true;
+
+        initOnFirstProcess();
+    }
+
+    m_askedToStop.store(0);
+
+    return processInternal(p_args, p_popupWidgetFunc);
+}
+
+void IUnitedEntry::stop()
+{
+    m_askedToStop.store(1);
+}
+
+bool IUnitedEntry::isAskedToStop() const
+{
+    return m_askedToStop.load() == 1;
+}
+
+void IUnitedEntry::setOngoing(bool p_ongoing)
+{
+    m_ongoing = p_ongoing;
+}
+
+bool IUnitedEntry::isOngoing() const
+{
+    return m_ongoing;
+}
+
+void IUnitedEntry::handleAction(Action p_act)
+{
+    auto widget = currentPopupWidget();
+    if (!widget) {
+        return;
+    }
+    handleActionCommon(p_act, widget.data());
+}
+
+void IUnitedEntry::handleActionCommon(Action p_act, QWidget *p_widget)
+{
+    switch (p_act)
+    {
+    case Action::NextItem:
+    {
+        auto eve = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier);
+        QCoreApplication::postEvent(p_widget, eve);
+        break;
+    }
+
+    case Action::PreviousItem:
+    {
+        auto eve = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier);
+        QCoreApplication::postEvent(p_widget, eve);
+        break;
+    }
+
+    case Action::Activate:
+    {
+        auto eve = new QKeyEvent(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier);
+        QCoreApplication::postEvent(p_widget, eve);
+        break;
+    }
+
+    case Action::LevelUp:
+    {
+        auto treeWidget = dynamic_cast<QTreeWidget *>(p_widget);
+        if (treeWidget) {
+            TreeWidget::selectParentItem(treeWidget);
+        }
+        break;
+    }
+
+    case Action::ExpandCollapse:
+    {
+        auto treeWidget = dynamic_cast<QTreeWidget *>(p_widget);
+        if (treeWidget) {
+            auto item = treeWidget->currentItem();
+            if (item) {
+                item->setExpanded(!item->isExpanded());
+            }
+        }
+        break;
+    }
+
+    case Action::ExpandCollapseAll:
+    {
+        auto treeWidget = dynamic_cast<QTreeWidget *>(p_widget);
+        if (treeWidget) {
+            if (TreeWidget::isExpanded(treeWidget)) {
+                treeWidget->collapseAll();
+            } else {
+                treeWidget->expandAll();
+            }
+        }
+        break;
+    }
+
+    default:
+        Q_ASSERT(false);
+        break;
+    }
+}

+ 79 - 0
src/unitedentry/iunitedentry.h

@@ -0,0 +1,79 @@
+#ifndef IUNITEDENTRY_H
+#define IUNITEDENTRY_H
+
+#include <QObject>
+
+#include <functional>
+
+#include <QAtomicInt>
+
+namespace vnotex
+{
+    class UnitedEntryMgr;
+
+    // Interface of a UnitedEntry.
+    class IUnitedEntry : public QObject
+    {
+        Q_OBJECT
+    public:
+        enum class Action
+        {
+            NextItem,
+            PreviousItem,
+            Activate,
+            LevelUp,
+            ExpandCollapse,
+            ExpandCollapseAll
+        };
+
+        IUnitedEntry(const QString &p_name,
+                     const QString &p_description,
+                     const UnitedEntryMgr *p_mgr,
+                     QObject *p_parent = nullptr);
+
+        const QString &name() const;
+
+        virtual QString description() const;
+
+        void process(const QString &p_args,
+                     const std::function<void(const QSharedPointer<QWidget> &)> &p_popupWidgetFunc);
+
+        virtual bool isOngoing() const;
+
+        virtual void stop();
+
+        virtual void handleAction(Action p_act);
+
+        virtual QSharedPointer<QWidget> currentPopupWidget() const = 0;
+
+        static void handleActionCommon(Action p_act, QWidget *p_widget);
+
+    signals:
+        void finished();
+
+    protected:
+        virtual void initOnFirstProcess() = 0;
+
+        virtual void processInternal(const QString &p_args,
+                                     const std::function<void(const QSharedPointer<QWidget> &)> &p_popupWidgetFunc) = 0;
+
+        bool isAskedToStop() const;
+
+        virtual void setOngoing(bool p_ongoing);
+
+        const UnitedEntryMgr *m_mgr = nullptr;
+
+    private:
+        bool m_initialized = false;
+
+        QString m_name;
+
+        QString m_description;
+
+        QAtomicInt m_askedToStop = 0;
+
+        bool m_ongoing = false;
+    };
+}
+
+#endif // IUNITEDENTRY_H

+ 487 - 0
src/unitedentry/unitedentry.cpp

@@ -0,0 +1,487 @@
+#include "unitedentry.h"
+
+#include <QHBoxLayout>
+#include <QSizePolicy>
+#include <QAction>
+#include <QFocusEvent>
+#include <QDebug>
+#include <QKeySequence>
+#include <QShortcut>
+#include <QApplication>
+#include <QTimer>
+#include <QTreeWidget>
+#include <QLabel>
+#include <QFont>
+#include <QMenu>
+
+#include <widgets/lineeditwithsnippet.h>
+#include <widgets/widgetsfactory.h>
+#include <widgets/propertydefs.h>
+#include <widgets/mainwindow.h>
+#include <core/configmgr.h>
+#include <core/coreconfig.h>
+#include <core/widgetconfig.h>
+#include <core/thememgr.h>
+#include <core/vnotex.h>
+#include <utils/widgetutils.h>
+#include <utils/iconutils.h>
+
+#include "entrypopup.h"
+#include "entrywidgetfactory.h"
+#include "unitedentrymgr.h"
+#include "iunitedentry.h"
+#include "unitedentryhelper.h"
+
+using namespace vnotex;
+
+UnitedEntry::UnitedEntry(QWidget *p_parent)
+    : QWidget(p_parent)
+{
+    m_processTimer = new QTimer(this);
+    m_processTimer->setSingleShot(true);
+    m_processTimer->setInterval(300);
+    connect(m_processTimer, &QTimer::timeout,
+            this, &UnitedEntry::processInput);
+
+    setupUI();
+
+    connect(qApp, &QApplication::focusChanged,
+            this, &UnitedEntry::handleFocusChanged);
+
+    connect(&UnitedEntryMgr::getInst(), &UnitedEntryMgr::entryFinished,
+            this, &UnitedEntry::handleEntryFinished);
+}
+
+UnitedEntry::~UnitedEntry()
+{
+}
+
+void UnitedEntry::setupUI()
+{
+    auto mainLayout = new QHBoxLayout(this);
+    mainLayout->setContentsMargins(0, 0, 0, 0);
+
+    setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Fixed);
+
+    // Shortcut.
+    const auto shortcut = ConfigMgr::getInst().getCoreConfig().getShortcut(CoreConfig::Shortcut::UnitedEntry);
+    const QKeySequence kseq(shortcut);
+    const auto shortcutText = kseq.isEmpty() ? QString() : kseq.toString(QKeySequence::NativeText);
+    if (!kseq.isEmpty()) {
+        auto sc = WidgetUtils::createShortcut(shortcut, this, Qt::ShortcutContext::ApplicationShortcut);
+        if (sc) {
+            connect(sc, &QShortcut::activated,
+                    this, [this]() {
+                        if (m_lineEdit->hasFocus()) {
+                            return;
+                        }
+
+                        bool popupVisible = m_popup->isVisible();
+                        if (popupVisible) {
+                            // Make m_lineEdit->setFocus() work.
+                            m_popup->hide();
+                        }
+                        m_lineEdit->setFocus();
+                        if (popupVisible) {
+                            m_popup->show();
+                        }
+                    });
+        }
+    }
+    setToolTip(shortcutText.isEmpty() ? tr("United Entry") : tr("United Entry (%1)").arg(shortcutText));
+
+    // Line edit.
+    m_lineEdit = WidgetsFactory::createLineEditWithSnippet(this);
+    mainLayout->addWidget(m_lineEdit);
+    m_lineEdit->setToolTip(QString());
+    m_lineEdit->setPlaceholderText(shortcutText.isEmpty() ? tr("Type to command") : tr("Type to command (%1)").arg(shortcutText));
+    m_lineEdit->setClearButtonEnabled(true);
+    m_lineEdit->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Fixed);
+    connect(m_lineEdit, &QLineEdit::textChanged,
+            m_processTimer, QOverload<void>::of(&QTimer::start));
+    setFocusProxy(m_lineEdit);
+
+    // Popup.
+    m_popup = new EntryPopup(this);
+    m_popup->installEventFilter(this);
+    m_popup->hide();
+
+    setupIcons();
+}
+
+void UnitedEntry::setupIcons()
+{
+    const auto &themeMgr = VNoteX::getInst().getThemeMgr();
+    const auto fg = themeMgr.paletteColor("widgets#unitedentry#icon#fg");
+    // Use QIcon::Disabled as the busy state.
+    const auto busyFg = themeMgr.paletteColor("widgets#unitedentry#icon#busy#fg");
+    QVector<IconUtils::OverriddenColor> colors;
+    colors.push_back(IconUtils::OverriddenColor(fg, QIcon::Normal));
+    colors.push_back(IconUtils::OverriddenColor(busyFg, QIcon::Disabled));
+
+    const auto icon = IconUtils::fetchIcon(themeMgr.getIconFile("united_entry.svg"), colors);
+    m_iconAction = m_lineEdit->addAction(icon, QLineEdit::ActionPosition::LeadingPosition);
+    m_iconAction->setText(tr("Options"));
+
+    // Menu.
+    auto menu = WidgetsFactory::createMenu(this);
+    m_iconAction->setMenu(menu);
+
+    {
+        auto expandAct = menu->addAction(tr("Expand All"),
+                                         this,
+                                         [this](bool checked) {
+                                            ConfigMgr::getInst().getWidgetConfig().setUnitedEntryExpandAllEnabled(checked);
+                                            UnitedEntryMgr::getInst().setExpandAllEnabled(checked);
+                                         });
+        expandAct->setCheckable(true);
+        expandAct->setChecked(ConfigMgr::getInst().getWidgetConfig().getUnitedEntryExpandAllEnabled());
+        UnitedEntryMgr::getInst().setExpandAllEnabled(expandAct->isChecked());
+    }
+
+    connect(m_iconAction, &QAction::triggered,
+            this, [this]() {
+                auto pos = mapToGlobal(QPoint(0, height()));
+                auto menu = m_iconAction->menu();
+                menu->exec(pos);
+            });
+}
+
+void UnitedEntry::activateUnitedEntry()
+{
+    if (m_activated) {
+        return;
+    }
+
+    if (!UnitedEntryMgr::getInst().isInitialized()) {
+        return;
+    }
+
+    m_activated = true;
+
+    setSizePolicy(QSizePolicy::Policy::MinimumExpanding, QSizePolicy::Policy::Fixed);
+
+    m_lineEdit->selectAll();
+    m_lineEdit->setFocus();
+
+    m_processTimer->start();
+}
+
+void UnitedEntry::deactivateUnitedEntry()
+{
+    if (!m_activated) {
+        return;
+    }
+
+    m_activated = false;
+    m_previousFocusWidget = nullptr;
+
+    setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Fixed);
+
+    m_popup->hide();
+}
+
+void UnitedEntry::handleFocusChanged(QWidget *p_old, QWidget *p_now)
+{
+    if (p_now == m_lineEdit) {
+        activateUnitedEntry();
+        if (!m_previousFocusWidget && p_old != this && !WidgetUtils::isOrAncestorOf(this, p_old)) {
+            m_previousFocusWidget = p_old;
+        }
+        return;
+    }
+
+    if (!m_activated) {
+        return;
+    }
+
+    if (!p_now || (p_now != this && !WidgetUtils::isOrAncestorOf(this, p_now))) {
+        deactivateUnitedEntry();
+    }
+}
+
+void UnitedEntry::keyPressEvent(QKeyEvent *p_event)
+{
+    const int key = p_event->key();
+    const int modifiers = p_event->modifiers();
+    IUnitedEntry::Action act = IUnitedEntry::Action::NextItem;
+    switch (key) {
+    case Qt::Key_BracketLeft:
+        if (!WidgetUtils::isViControlModifier(modifiers)) {
+            break;
+        }
+        Q_FALLTHROUGH();
+    case Qt::Key_Escape:
+        exitUnitedEntry();
+        break;
+
+    // Up/Down Ctrl+K/J to navigate to previous/next item.
+    case Qt::Key_Up:
+        act = IUnitedEntry::Action::PreviousItem;
+        Q_FALLTHROUGH();
+    case Qt::Key_Down:
+        if (m_lastEntry) {
+            m_lastEntry->handleAction(act);
+        } else if (m_entryListWidget && m_entryListWidget->isVisible()) {
+            IUnitedEntry::handleActionCommon(act, m_entryListWidget.data());
+        }
+        break;
+
+    case Qt::Key_K:
+        act = IUnitedEntry::Action::PreviousItem;
+        Q_FALLTHROUGH();
+    case Qt::Key_J:
+        if (WidgetUtils::isViControlModifier(modifiers)) {
+            if (m_lastEntry) {
+                m_lastEntry->handleAction(act);
+            } else if (m_entryListWidget && m_entryListWidget->isVisible()) {
+                IUnitedEntry::handleActionCommon(act, m_entryListWidget.data());
+            }
+        }
+        break;
+
+    case Qt::Key_Enter:
+        Q_FALLTHROUGH();
+    case Qt::Key_Return:
+        if (m_lastEntry) {
+            m_lastEntry->handleAction(IUnitedEntry::Action::Activate);
+        }
+        break;
+
+    case Qt::Key_E:
+        if (WidgetUtils::isViControlModifier(modifiers)) {
+            // Eliminate input till the entry name.
+            const auto text = m_lineEdit->evaluatedText();
+            const auto entry = UnitedEntryHelper::parseUserEntry(text);
+            if (!entry.m_name.isEmpty()) {
+                m_lineEdit->setText(entry.m_name + QLatin1Char(' '));
+            }
+        }
+        break;
+
+    case Qt::Key_F:
+        if (WidgetUtils::isViControlModifier(modifiers)) {
+            // Select the entry name.
+            const auto text = m_lineEdit->evaluatedText();
+            const auto entry = UnitedEntryHelper::parseUserEntry(text);
+            if (!entry.m_name.isEmpty()) {
+                m_lineEdit->setSelection(0, entry.m_name.size());
+            }
+        }
+        break;
+
+    case Qt::Key_D:
+        if (WidgetUtils::isViControlModifier(modifiers)) {
+            // Stop the entry.
+            if (m_lastEntry) {
+                m_lastEntry->stop();
+            }
+        }
+        break;
+
+    case Qt::Key_L:
+        if (WidgetUtils::isViControlModifier(modifiers)) {
+            // Go up one level.
+            if (m_lastEntry) {
+                m_lastEntry->handleAction(IUnitedEntry::Action::LevelUp);
+            }
+        }
+        break;
+
+    case Qt::Key_I:
+        if (WidgetUtils::isViControlModifier(modifiers)) {
+            // Expand/Collapse the item.
+            if (m_lastEntry) {
+                m_lastEntry->handleAction(IUnitedEntry::Action::ExpandCollapse);
+            }
+        }
+        break;
+
+    case Qt::Key_B:
+        if (WidgetUtils::isViControlModifier(modifiers)) {
+            // Expand/Collapse all the items.
+            if (m_lastEntry) {
+                m_lastEntry->handleAction(IUnitedEntry::Action::ExpandCollapseAll);
+            }
+        }
+        break;
+
+    default:
+        break;
+    }
+
+    QWidget::keyPressEvent(p_event);
+}
+
+void UnitedEntry::clear()
+{
+    m_lineEdit->setFocus();
+    m_lineEdit->clear();
+}
+
+void UnitedEntry::processInput()
+{
+    m_hasPending = false;
+
+    if (!m_activated) {
+        return;
+    }
+
+    if (m_iconAction->menu()->isVisible()) {
+        // Do not display the popup which will hide the menu.
+        return;
+    }
+
+    if (m_lastEntry && m_lastEntry->isOngoing()) {
+        m_hasPending = true;
+        m_lastEntry->stop();
+        return;
+    }
+
+    const auto text = m_lineEdit->evaluatedText();
+    const auto entry = UnitedEntryHelper::parseUserEntry(text);
+    if (entry.m_name.isEmpty()) {
+        filterEntryListWidgetEntries(entry.m_name);
+        popupWidget(getEntryListWidget());
+        m_lastEntry.clear();
+        return;
+    }
+
+    // Filter the help widget if space after entry name is not entered yet.
+    if (entry.m_name == text.trimmed() && !text.back().isSpace()) {
+        if (filterEntryListWidgetEntries(entry.m_name)) {
+            popupWidget(getEntryListWidget());
+            m_lastEntry.clear();
+            return;
+        }
+    } else {
+        if (!m_lastEntry || m_lastEntry->name() == entry.m_name) {
+            m_lastEntry = UnitedEntryMgr::getInst().findEntry(entry.m_name);
+        }
+
+        if (m_lastEntry) {
+            // Found.
+            setBusy(true);
+            m_lastEntry->process(entry.m_args, std::bind(&UnitedEntry::popupWidget, this, std::placeholders::_1));
+            return;
+        }
+    }
+
+    // No entry found.
+    popupWidget(getInfoWidget(tr("Unknown entry: %1").arg(entry.m_name)));
+    m_lastEntry.clear();
+}
+
+void UnitedEntry::popupWidget(const QSharedPointer<QWidget> &p_widget)
+{
+    m_popup->setWidget(p_widget);
+    m_popup->show();
+}
+
+const QSharedPointer<QTreeWidget> &UnitedEntry::getEntryListWidget()
+{
+    if (!m_entryListWidget) {
+        m_entryListWidget = EntryWidgetFactory::createTreeWidget(2);
+        m_entryListWidget->setHeaderHidden(false);
+        m_entryListWidget->setHeaderLabels(QStringList() << tr("Entry") << tr("Description"));
+
+        const auto entries = UnitedEntryMgr::getInst().getEntries();
+        for (const auto &entry : entries) {
+            m_entryListWidget->addTopLevelItem(new QTreeWidgetItem({entry->name(), entry->description()}));
+        }
+    }
+
+    return m_entryListWidget;
+}
+
+QSharedPointer<QLabel> UnitedEntry::getInfoWidget(const QString &p_info)
+{
+    return EntryWidgetFactory::createLabel(p_info);
+}
+
+void UnitedEntry::resizeEvent(QResizeEvent *p_event)
+{
+    QWidget::resizeEvent(p_event);
+
+    updatePopupGeometry();
+}
+
+void UnitedEntry::updatePopupGeometry()
+{
+    m_popup->updateGeometryToContents();
+}
+
+bool UnitedEntry::filterEntryListWidgetEntries(const QString &p_name)
+{
+    const auto &entryListWidget = getEntryListWidget();
+
+    if (p_name.isEmpty()) {
+        for (int i = 0; i < entryListWidget->topLevelItemCount(); ++i) {
+            entryListWidget->topLevelItem(i)->setHidden(false);
+        }
+        return true;
+    }
+
+    auto items = entryListWidget->findItems(p_name, Qt::MatchStartsWith);
+    for (int i = 0; i < entryListWidget->topLevelItemCount(); ++i) {
+        entryListWidget->topLevelItem(i)->setHidden(true);
+    }
+    for (const auto &item : items) {
+        item->setHidden(false);
+    }
+    return !items.isEmpty();
+}
+
+void UnitedEntry::handleEntryFinished(IUnitedEntry *p_entry)
+{
+    if (p_entry != m_lastEntry.data()) {
+        return;
+    }
+
+    setBusy(false);
+
+    if (m_hasPending) {
+        m_processTimer->start();
+    }
+}
+
+void UnitedEntry::setBusy(bool p_busy)
+{
+    m_iconAction->setEnabled(!p_busy);
+}
+
+bool UnitedEntry::eventFilter(QObject *p_watched, QEvent *p_event)
+{
+    if (p_watched == m_popup) {
+        if (p_event->type() == QEvent::KeyPress) {
+            auto eve = static_cast<QKeyEvent *>(p_event);
+            switch (eve->key()) {
+            case Qt::Key_BracketLeft:
+                if (!WidgetUtils::isViControlModifier(eve->modifiers())) {
+                    break;
+                }
+                Q_FALLTHROUGH();
+            case Qt::Key_Escape:
+                exitUnitedEntry();
+                // Need to call deactivateUnitedEntry() again since focusChanged is not triggered.
+                deactivateUnitedEntry();
+                return true;
+
+            default:
+                break;
+            }
+        }
+    }
+
+    return QWidget::eventFilter(p_watched, p_event);
+}
+
+void UnitedEntry::exitUnitedEntry()
+{
+    if (m_previousFocusWidget) {
+        // Deactivate and focus previous widget.
+        m_previousFocusWidget->setFocus();
+    } else {
+        VNoteX::getInst().getMainWindow()->setFocus();
+    }
+}

+ 85 - 0
src/unitedentry/unitedentry.h

@@ -0,0 +1,85 @@
+#ifndef UNITEDENTRY_H
+#define UNITEDENTRY_H
+
+#include <QWidget>
+#include <QSharedPointer>
+
+class QAction;
+class QTimer;
+class QTreeWidget;
+class QLabel;
+
+namespace vnotex
+{
+    class LineEditWithSnippet;
+    class EntryPopup;
+    class IUnitedEntry;
+
+    class UnitedEntry : public QWidget
+    {
+        Q_OBJECT
+    public:
+        explicit UnitedEntry(QWidget *p_parent = nullptr);
+
+        ~UnitedEntry();
+
+        bool eventFilter(QObject *p_watched, QEvent *p_event) Q_DECL_OVERRIDE;
+
+    protected:
+        void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
+
+        void resizeEvent(QResizeEvent *p_event) Q_DECL_OVERRIDE;
+
+    private:
+        void setupUI();
+
+        void setupIcons();
+
+        void activateUnitedEntry();
+
+        void deactivateUnitedEntry();
+
+        void handleFocusChanged(QWidget *p_old, QWidget *p_now);
+
+        void clear();
+
+        void processInput();
+
+        const QSharedPointer<QTreeWidget> &getEntryListWidget();
+
+        QSharedPointer<QLabel> getInfoWidget(const QString &p_info);
+
+        void updatePopupGeometry();
+
+        void popupWidget(const QSharedPointer<QWidget> &p_widget);
+
+        // Return true if there is any entry visible.
+        bool filterEntryListWidgetEntries(const QString &p_name);
+
+        void handleEntryFinished(IUnitedEntry *p_entry);
+
+        void setBusy(bool p_busy);
+
+        void exitUnitedEntry();
+
+        LineEditWithSnippet *m_lineEdit = nullptr;
+
+        EntryPopup *m_popup = nullptr;
+
+        QAction *m_iconAction = nullptr;
+
+        bool m_activated = false;
+
+        QWidget *m_previousFocusWidget = nullptr;
+
+        QTimer *m_processTimer = nullptr;
+
+        QSharedPointer<QTreeWidget> m_entryListWidget = nullptr;
+
+        QSharedPointer<IUnitedEntry> m_lastEntry;
+
+        bool m_hasPending = false;
+    };
+}
+
+#endif // UNITEDENTRY_H

+ 24 - 0
src/unitedentry/unitedentry.pri

@@ -0,0 +1,24 @@
+QT += widgets
+
+HEADERS += \
+    $$PWD/entrypopup.h \
+    $$PWD/entrywidgetfactory.h \
+    $$PWD/findunitedentry.h \
+    $$PWD/helpunitedentry.h \
+    $$PWD/iunitedentry.h \
+    $$PWD/unitedentry.h \
+    $$PWD/unitedentryalias.h \
+    $$PWD/unitedentryhelper.h \
+    $$PWD/unitedentrymgr.h
+
+SOURCES += \
+    $$PWD/entrypopup.cpp \
+    $$PWD/entrywidgetfactory.cpp \
+    $$PWD/findunitedentry.cpp \
+    $$PWD/helpunitedentry.cpp \
+    $$PWD/iunitedentry.cpp \
+    $$PWD/unitedentry.cpp \
+    $$PWD/unitedentryalias.cpp \
+    $$PWD/unitedentryhelper.cpp \
+    $$PWD/unitedentrymgr.cpp
+

+ 99 - 0
src/unitedentry/unitedentryalias.cpp

@@ -0,0 +1,99 @@
+#include "unitedentryalias.h"
+
+#include <QDebug>
+#include <QLabel>
+
+#include "unitedentrymgr.h"
+#include "entrywidgetfactory.h"
+
+using namespace vnotex;
+
+UnitedEntryAlias::UnitedEntryAlias(const QString &p_name,
+                                   const QString &p_description,
+                                   const QString &p_value,
+                                   const UnitedEntryMgr *p_mgr,
+                                   QObject *p_parent)
+    : IUnitedEntry(p_name, p_description, p_mgr, p_parent),
+      m_value(p_value)
+{
+    m_alias = UnitedEntryHelper::parseUserEntry(m_value);
+}
+
+UnitedEntryAlias::UnitedEntryAlias(const QJsonObject &p_obj,
+                                   const UnitedEntryMgr *p_mgr,
+                                   QObject *p_parent)
+    : UnitedEntryAlias(p_obj[QStringLiteral("name")].toString(),
+                       p_obj[QStringLiteral("description")].toString(),
+                       p_obj[QStringLiteral("value")].toString(),
+                       p_mgr,
+                       p_parent)
+{
+}
+
+QString UnitedEntryAlias::description() const
+{
+    return tr("[Alias] ") + IUnitedEntry::description();
+}
+
+QJsonObject UnitedEntryAlias::toJson() const
+{
+    QJsonObject obj;
+    obj[QStringLiteral("name")] = name();
+    obj[QStringLiteral("description")] = description();
+    obj[QStringLiteral("value")] = m_value;
+    return obj;
+}
+
+void UnitedEntryAlias::initOnFirstProcess()
+{
+    m_realEntry = m_mgr->findEntry(m_alias.m_name).data();
+    if (!m_realEntry) {
+        qWarning() << "invalid UnitedEntry alias" << name() << m_value;
+    } else {
+        connect(m_realEntry, &IUnitedEntry::finished,
+                this, &IUnitedEntry::finished);
+    }
+}
+
+void UnitedEntryAlias::processInternal(const QString &p_args,
+                                       const std::function<void(const QSharedPointer<QWidget> &)> &p_popupWidgetFunc)
+{
+    if (!m_realEntry) {
+        auto label = EntryWidgetFactory::createLabel(tr("Invalid UnitedEntry alias: %1").arg(m_value));
+        p_popupWidgetFunc(label);
+        emit finished();
+        return;
+    }
+
+    m_realEntry->process(m_alias.m_args + " " + p_args, p_popupWidgetFunc);
+}
+
+bool UnitedEntryAlias::isOngoing() const
+{
+    if (m_realEntry) {
+        return m_realEntry->isOngoing();
+    }
+    return false;
+}
+
+void UnitedEntryAlias::setOngoing(bool p_ongoing)
+{
+    Q_UNUSED(p_ongoing);
+    Q_ASSERT(false);
+}
+
+void UnitedEntryAlias::handleAction(Action p_act)
+{
+    if (m_realEntry) {
+        m_realEntry->handleAction(p_act);
+    }
+}
+
+QSharedPointer<QWidget> UnitedEntryAlias::currentPopupWidget() const
+{
+    if (m_realEntry) {
+        return m_realEntry->currentPopupWidget();
+    }
+
+    return nullptr;
+}

+ 56 - 0
src/unitedentry/unitedentryalias.h

@@ -0,0 +1,56 @@
+#ifndef UNITEDENTRYALIAS_H
+#define UNITEDENTRYALIAS_H
+
+#include "iunitedentry.h"
+
+#include <QJsonObject>
+
+#include "unitedentryhelper.h"
+
+namespace vnotex
+{
+    class UnitedEntryMgr;
+
+    // UnitedEntry which points to another UnitedEntry.
+    class UnitedEntryAlias : public IUnitedEntry
+    {
+        Q_OBJECT
+    public:
+        UnitedEntryAlias(const QString &p_name,
+                         const QString &p_description,
+                         const QString &p_value,
+                         const UnitedEntryMgr *p_mgr,
+                         QObject *p_parent = nullptr);
+
+        UnitedEntryAlias(const QJsonObject &p_obj,
+                         const UnitedEntryMgr *p_mgr,
+                         QObject *p_parent = nullptr);
+
+        QJsonObject toJson() const;
+
+        QString description() const Q_DECL_OVERRIDE;
+
+        bool isOngoing() const Q_DECL_OVERRIDE;
+
+        void handleAction(Action p_act) Q_DECL_OVERRIDE;
+
+        QSharedPointer<QWidget> currentPopupWidget() const Q_DECL_OVERRIDE;
+
+    protected:
+        void initOnFirstProcess() Q_DECL_OVERRIDE;
+
+        void processInternal(const QString &p_args,
+                             const std::function<void(const QSharedPointer<QWidget> &)> &p_popupWidgetFunc) Q_DECL_OVERRIDE;
+
+        void setOngoing(bool p_ongoing) Q_DECL_OVERRIDE;
+
+    private:
+        QString m_value;
+
+        UnitedEntryHelper::UserEntry m_alias;
+
+        IUnitedEntry *m_realEntry = nullptr;
+    };
+}
+
+#endif // UNITEDENTRYALIAS_H

+ 70 - 0
src/unitedentry/unitedentryhelper.cpp

@@ -0,0 +1,70 @@
+#include "unitedentryhelper.h"
+
+#include <QIcon>
+
+#include <core/vnotex.h>
+#include <core/thememgr.h>
+#include <utils/iconutils.h>
+
+using namespace vnotex;
+
+UnitedEntryHelper::UserEntry UnitedEntryHelper::parseUserEntry(const QString &p_text)
+{
+    UserEntry entry;
+
+    const auto text = p_text.trimmed();
+
+    if (text.isEmpty()) {
+        return entry;
+    }
+
+    int idx = text.indexOf(QLatin1Char(' '));
+    if (idx == -1) {
+        entry.m_name = text.toLower();
+    } else {
+        entry.m_name = text.left(idx).toLower();
+        entry.m_args = text.mid(idx).trimmed();
+    }
+
+    return entry;
+}
+
+const QIcon &UnitedEntryHelper::itemIcon(ItemType p_type)
+{
+    static QIcon icons[ItemType::MaxItemType];
+
+    if (icons[0].isNull()) {
+        // Init.
+        const QString nodeIconFgName = "base#icon#fg";
+        const auto &themeMgr = VNoteX::getInst().getThemeMgr();
+        const auto fg = themeMgr.paletteColor(nodeIconFgName);
+
+        icons[ItemType::Buffer] = IconUtils::fetchIcon(themeMgr.getIconFile("buffer.svg"), fg);
+        icons[ItemType::File] = IconUtils::fetchIcon(themeMgr.getIconFile("file_node.svg"), fg);
+        icons[ItemType::Folder] = IconUtils::fetchIcon(themeMgr.getIconFile("folder_node.svg"), fg);
+        icons[ItemType::Notebook] = IconUtils::fetchIcon(themeMgr.getIconFile("notebook_default.svg"), fg);
+        icons[ItemType::Other] = IconUtils::fetchIcon(themeMgr.getIconFile("other_item.svg"), fg);
+    }
+
+    return icons[p_type];
+}
+
+UnitedEntryHelper::ItemType UnitedEntryHelper::locationTypeToItemType(LocationType p_type)
+{
+    switch (p_type) {
+    case LocationType::Buffer:
+        return ItemType::Buffer;
+
+    case LocationType::File:
+        return ItemType::File;
+
+    case LocationType::Folder:
+        return ItemType::Folder;
+
+    case LocationType::Notebook:
+        return ItemType::Notebook;
+
+    default:
+        return ItemType::Other;
+    }
+}

+ 43 - 0
src/unitedentry/unitedentryhelper.h

@@ -0,0 +1,43 @@
+#ifndef UNITEDENTRYHELPER_H
+#define UNITEDENTRYHELPER_H
+
+#include <QString>
+
+#include <core/location.h>
+
+class QIcon;
+
+namespace vnotex
+{
+    class UnitedEntryHelper
+    {
+    public:
+        struct UserEntry
+        {
+            QString m_name;
+
+            QString m_args;
+        };
+
+        enum ItemType
+        {
+            Buffer,
+            File,
+            Folder,
+            Notebook,
+            Other,
+            MaxItemType
+        };
+
+        UnitedEntryHelper() = delete;
+
+        static UserEntry parseUserEntry(const QString &p_text);
+
+        static const QIcon &itemIcon(ItemType p_type);
+
+        static ItemType locationTypeToItemType(LocationType p_type);
+    };
+
+}
+
+#endif // UNITEDENTRYHELPER_H

+ 80 - 0
src/unitedentry/unitedentrymgr.cpp

@@ -0,0 +1,80 @@
+#include "unitedentrymgr.h"
+
+#include "findunitedentry.h"
+#include "helpunitedentry.h"
+#include "unitedentryalias.h"
+
+#include <core/configmgr.h>
+#include <core/coreconfig.h>
+#include <core/vnotex.h>
+#include <widgets/searchinfoprovider.h>
+
+using namespace vnotex;
+
+UnitedEntryMgr::UnitedEntryMgr(QObject *p_parent)
+    : QObject(p_parent)
+{
+}
+
+void UnitedEntryMgr::init()
+{
+    if (m_initialized) {
+        return;
+    }
+
+    m_initialized = true;
+
+    // Built-in entries.
+    const auto mainWindow = VNoteX::getInst().getMainWindow();
+    addEntry(QSharedPointer<FindUnitedEntry>::create(SearchInfoProvider::create(mainWindow), this));
+
+    addEntry(QSharedPointer<HelpUnitedEntry>::create(this));
+
+    // Alias from config.
+    const auto &config = ConfigMgr::getInst().getCoreConfig();
+    const auto &aliasArr = config.getUnitedEntryAlias();
+    for (int i = 0; i < aliasArr.size(); ++i) {
+        auto entry = QSharedPointer<UnitedEntryAlias>::create(aliasArr[i].toObject(), this);
+        addEntry(entry);
+    }
+}
+
+void UnitedEntryMgr::addEntry(const QSharedPointer<IUnitedEntry> &p_entry)
+{
+    Q_ASSERT(!m_entries.contains(p_entry->name()));
+    m_entries.insert(p_entry->name(), p_entry);
+    connect(p_entry.data(), &IUnitedEntry::finished,
+            this, [this]() {
+                emit entryFinished(reinterpret_cast<IUnitedEntry *>(sender()));
+            });
+}
+
+QList<QSharedPointer<IUnitedEntry>> UnitedEntryMgr::getEntries() const
+{
+    return m_entries.values();
+}
+
+QSharedPointer<IUnitedEntry> UnitedEntryMgr::findEntry(const QString &p_name) const
+{
+    auto it = m_entries.find(p_name);
+    if (it == m_entries.end()) {
+        return nullptr;
+    }
+
+    return it.value();
+}
+
+bool UnitedEntryMgr::isInitialized() const
+{
+    return m_initialized;
+}
+
+bool UnitedEntryMgr::getExpandAllEnabled() const
+{
+    return m_expandAllEnabled;
+}
+
+void UnitedEntryMgr::setExpandAllEnabled(bool p_enabled)
+{
+    m_expandAllEnabled = p_enabled;
+}

+ 52 - 0
src/unitedentry/unitedentrymgr.h

@@ -0,0 +1,52 @@
+#ifndef UNITEDENTRYMGR_H
+#define UNITEDENTRYMGR_H
+
+#include <QObject>
+#include <QSharedPointer>
+#include <QMap>
+
+#include <core/noncopyable.h>
+
+namespace vnotex
+{
+    class IUnitedEntry;
+
+    class UnitedEntryMgr : public QObject, private Noncopyable
+    {
+        Q_OBJECT
+    public:
+        static UnitedEntryMgr &getInst()
+        {
+            static UnitedEntryMgr inst;
+            inst.init();
+            return inst;
+        }
+
+        void init();
+
+        QList<QSharedPointer<IUnitedEntry>> getEntries() const;
+
+        QSharedPointer<IUnitedEntry> findEntry(const QString &p_name) const;
+
+        bool isInitialized() const;
+
+        bool getExpandAllEnabled() const;
+        void setExpandAllEnabled(bool p_enabled);
+
+    signals:
+        void entryFinished(IUnitedEntry *p_entry);
+
+    private:
+        explicit UnitedEntryMgr(QObject *p_parent = nullptr);
+
+        void addEntry(const QSharedPointer<IUnitedEntry> &p_entry);
+
+        bool m_initialized = false;
+
+        QMap<QString, QSharedPointer<IUnitedEntry>> m_entries;
+
+        bool m_expandAllEnabled = false;
+    };
+}
+
+#endif // UNITEDENTRYMGR_H

+ 1 - 0
src/utils/asyncworker.h

@@ -2,6 +2,7 @@
 #define ASYNCWORKER_H
 
 #include <QThread>
+
 #include <QAtomicInt>
 
 namespace vnotex

+ 20 - 0
src/utils/widgetutils.cpp

@@ -468,3 +468,23 @@ void WidgetUtils::clearLayout(QFormLayout *p_layout)
         p_layout->removeRow(i);
     }
 }
+
+// Different from QWidget::isAncestorOf(): unnecessary to be within the same window.
+bool WidgetUtils::isOrAncestorOf(const QWidget *p_widget, const QWidget *p_child)
+{
+    Q_ASSERT(p_widget);
+
+    if (!p_child) {
+        return false;
+    }
+
+    const QWidget *pa = p_child;
+    while (pa) {
+        if (pa == p_widget) {
+            return true;
+        }
+        pa = pa->parentWidget();
+    }
+
+    return false;
+}

+ 2 - 0
src/utils/widgetutils.h

@@ -93,6 +93,8 @@ namespace vnotex
 
         static void clearLayout(QFormLayout *p_layout);
 
+        static bool isOrAncestorOf(const QWidget *p_widget, const QWidget *p_child);
+
     private:
         static void resizeToHideScrollBar(QScrollArea *p_scroll, bool p_vertical, bool p_horizontal);
     };

+ 5 - 0
src/widgets/lineedit.cpp

@@ -135,3 +135,8 @@ void LineEdit::updateInputMethod() const
     // Ask input method to query current state, which will call inputMethodQuery().
     im->update(Qt::ImEnabled);
 }
+
+QRect LineEdit::cursorRect() const
+{
+    return QLineEdit::cursorRect();
+}

+ 2 - 0
src/widgets/lineedit.h

@@ -18,6 +18,8 @@ namespace vnotex
 
         void setInputMethodEnabled(bool p_enabled);
 
+        QRect cursorRect() const;
+
         static void selectBaseName(QLineEdit *p_lineEdit);
 
     protected:

+ 1 - 0
src/widgets/locationlist.cpp

@@ -140,6 +140,7 @@ void LocationList::addLocation(const ComplexLocation &p_location)
     item->setText(Columns::PathColumn, p_location.m_displayPath);
     item->setIcon(Columns::PathColumn, getItemIcon(p_location.m_type));
     item->setData(Columns::PathColumn, Qt::UserRole, p_location.m_path);
+    item->setToolTip(Columns::PathColumn, p_location.m_path);
 
     if (p_location.m_lines.size() == 1) {
         setItemLocationLineAndText(item, p_location.m_lines[0]);

+ 6 - 0
src/widgets/mainwindow.cpp

@@ -551,6 +551,12 @@ void MainWindow::focusViewArea()
     m_viewArea->focus();
 }
 
+NotebookExplorer *MainWindow::getNotebookExplorer() const
+{
+    Q_ASSERT(m_notebookExplorer);
+    return m_notebookExplorer;
+}
+
 void MainWindow::setupToolBar()
 {
     const int sz = ConfigMgr::getInst().getCoreConfig().getToolBarIconSize();

+ 2 - 0
src/widgets/mainwindow.h

@@ -53,6 +53,8 @@ namespace vnotex
 
         ViewArea *getViewArea() const;
 
+        NotebookExplorer *getNotebookExplorer() const;
+
         void setContentAreaExpanded(bool p_expanded);
         // Should be called after MainWindow is shown.
         bool isContentAreaExpanded() const;

+ 13 - 4
src/widgets/searchinfoprovider.cpp

@@ -1,17 +1,19 @@
 #include "searchinfoprovider.h"
 
+#include <core/vnotex.h>
 #include "viewarea.h"
 #include "notebookexplorer.h"
 #include "notebookmgr.h"
+#include "mainwindow.h"
 
 using namespace vnotex;
 
 SearchInfoProvider::SearchInfoProvider(const ViewArea *p_viewArea,
-                                       const NotebookExplorer *p_notebookExplorer,
-                                       const NotebookMgr *p_notebookMgr)
+        const NotebookExplorer *p_notebookExplorer,
+        const NotebookMgr *p_notebookMgr)
     : m_viewArea(p_viewArea),
-      m_notebookExplorer(p_notebookExplorer),
-      m_notebookMgr(p_notebookMgr)
+    m_notebookExplorer(p_notebookExplorer),
+    m_notebookMgr(p_notebookMgr)
 {
 }
 
@@ -41,3 +43,10 @@ QVector<Notebook *> SearchInfoProvider::getNotebooks() const
 
     return nbs;
 }
+
+QSharedPointer<SearchInfoProvider> SearchInfoProvider::create(const MainWindow *p_mainWindow)
+{
+    return QSharedPointer<SearchInfoProvider>::create(p_mainWindow->getViewArea(),
+                                                      p_mainWindow->getNotebookExplorer(),
+                                                      &VNoteX::getInst().getNotebookMgr());
+}

+ 6 - 1
src/widgets/searchinfoprovider.h

@@ -1,13 +1,16 @@
 #ifndef SEARCHINFOPROVIDER_H
 #define SEARCHINFOPROVIDER_H
 
-#include "searchpanel.h"
+#include <search/isearchinfoprovider.h>
+
+#include <QSharedPointer>
 
 namespace vnotex
 {
     class ViewArea;
     class NotebookExplorer;
     class NotebookMgr;
+    class MainWindow;
 
     class SearchInfoProvider : public ISearchInfoProvider
     {
@@ -24,6 +27,8 @@ namespace vnotex
 
         QVector<Notebook *> getNotebooks() const Q_DECL_OVERRIDE;
 
+        static QSharedPointer<SearchInfoProvider> create(const MainWindow *p_mainWindow);
+
     private:
         const ViewArea *m_viewArea = nullptr;
 

+ 7 - 91
src/widgets/searchpanel.cpp

@@ -29,6 +29,8 @@
 #include "mainwindow.h"
 #include <search/searchtoken.h>
 #include <search/searchresultitem.h>
+#include <search/isearchinfoprovider.h>
+#include <search/searchhelper.h>
 #include <utils/widgetutils.h>
 #include "locationlist.h"
 
@@ -305,7 +307,11 @@ void SearchPanel::startSearch()
 
     saveFields(*m_option);
 
-    auto state = search(m_option);
+    QString msg;
+    auto state = SearchHelper::searchOnProvider(getSearcher(), m_option, m_provider, msg);
+    if (!msg.isEmpty()) {
+        appendLog(msg);
+    }
 
     // On end.
     handleSearchFinished(state);
@@ -313,8 +319,6 @@ void SearchPanel::startSearch()
 
 void SearchPanel::handleSearchFinished(SearchState p_state)
 {
-    qDebug() << "handleSearchFinished" << (int)p_state;
-
     Q_ASSERT(m_searchOngoing);
     Q_ASSERT(p_state != SearchState::Idle);
 
@@ -420,94 +424,6 @@ void SearchPanel::saveFields(SearchOption &p_option)
     }
 }
 
-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) {
-            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) {

+ 1 - 24
src/widgets/searchpanel.h

@@ -3,7 +3,6 @@
 
 #include <QFrame>
 #include <QSharedPointer>
-#include <QList>
 
 #include <search/searchdata.h>
 #include <search/searcher.h>
@@ -21,28 +20,10 @@ class QVBoxLayout;
 namespace vnotex
 {
     class TitleBar;
-    class Buffer;
-    class Node;
-    class Notebook;
     class LocationList;
     struct Location;
     class SearchToken;
-
-    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 ISearchInfoProvider;
 
     class SearchPanel : public QFrame
     {
@@ -85,10 +66,6 @@ namespace vnotex
 
         void clearLog();
 
-        SearchState search(const QSharedPointer<SearchOption> &p_option);
-
-        bool isSearchOptionValid(const SearchOption &p_option);
-
         Searcher *getSearcher();
 
         void prepareLocationList();

+ 30 - 33
src/widgets/toolbarhelper.cpp

@@ -28,6 +28,7 @@
 #include <core/htmltemplatehelper.h>
 #include <core/exception.h>
 #include <task/taskmgr.h>
+#include <unitedentry/unitedentry.h>
 #include "propertydefs.h"
 #include "dialogs/settings/settingsdialog.h"
 #include "dialogs/updater.h"
@@ -279,6 +280,35 @@ QToolBar *ToolBarHelper::setupQuickAccessToolBar(MainWindow *p_win, QToolBar *p_
         tb->addWidget(toolBtn);
     }
 
+    // Task.
+    {
+        auto act = tb->addAction(generateIcon("task_menu.svg"), MainWindow::tr("Task"));
+        auto btn = dynamic_cast<QToolButton *>(tb->widgetForAction(act));
+        btn->setPopupMode(QToolButton::InstantPopup);
+        btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
+
+        auto taskMenu = WidgetsFactory::createMenu(tb);
+        setupTaskActionMenu(taskMenu);
+        btn->setMenu(taskMenu);
+        MainWindow::connect(taskMenu, &QMenu::triggered,
+                            taskMenu, [](QAction *act) {
+            auto task = reinterpret_cast<Task *>(act->data().toULongLong());
+            if (task) {
+                task->run();
+            }
+        });
+        MainWindow::connect(&VNoteX::getInst().getTaskMgr(), &TaskMgr::tasksUpdated,
+                            taskMenu, [taskMenu]() {
+            setupTaskMenu(taskMenu);
+        });
+    }
+
+    // United Entry.
+    {
+        auto ueEdit = new UnitedEntry(tb);
+        tb->addWidget(ueEdit);
+    }
+
     return tb;
 }
 
@@ -361,36 +391,6 @@ void ToolBarHelper::addTaskMenu(QMenu *p_menu, Task *p_task)
     WidgetUtils::addActionShortcut(action, p_task->getShortcut());
 }
 
-QToolBar *ToolBarHelper::setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar)
-{
-    auto tb = p_toolBar;
-    if (!tb) {
-        tb = createToolBar(p_win, MainWindow::tr("Task"), "TaskToolBar");
-    }
-
-    auto act = tb->addAction(generateIcon("task_menu.svg"), MainWindow::tr("Task"));
-    auto btn = dynamic_cast<QToolButton *>(tb->widgetForAction(act));
-    btn->setPopupMode(QToolButton::InstantPopup);
-    btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
-
-    auto taskMenu = WidgetsFactory::createMenu(tb);
-    setupTaskActionMenu(taskMenu);
-    btn->setMenu(taskMenu);
-    MainWindow::connect(taskMenu, &QMenu::triggered,
-                        taskMenu, [](QAction *act) {
-        auto task = reinterpret_cast<Task *>(act->data().toULongLong());
-        if (task) {
-            task->run();
-        }
-    });
-    MainWindow::connect(&VNoteX::getInst().getTaskMgr(), &TaskMgr::tasksUpdated,
-                        taskMenu, [taskMenu]() {
-        setupTaskMenu(taskMenu);
-    });
-
-    return tb;
-}
-
 QToolBar *ToolBarHelper::setupSettingsToolBar(MainWindow *p_win, QToolBar *p_toolBar)
 {
     auto tb = p_toolBar;
@@ -453,8 +453,6 @@ void ToolBarHelper::setupToolBars(MainWindow *p_mainWindow)
 
     setupQuickAccessToolBar(p_mainWindow, nullptr);
 
-    setupTaskToolBar(p_mainWindow, nullptr);
-
     setupSettingsToolBar(p_mainWindow, nullptr);
 }
 
@@ -466,7 +464,6 @@ void ToolBarHelper::setupToolBars(MainWindow *p_mainWindow, QToolBar *p_toolBar)
 
     setupFileToolBar(p_mainWindow, p_toolBar);
     setupQuickAccessToolBar(p_mainWindow, p_toolBar);
-    setupTaskToolBar(p_mainWindow, p_toolBar);
     setupSettingsToolBar(p_mainWindow, p_toolBar);
 }
 

+ 0 - 2
src/widgets/toolbarhelper.h

@@ -40,8 +40,6 @@ namespace vnotex
 
         static void addTaskMenu(QMenu *p_menu, Task *p_task);
 
-        static QToolBar *setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar);
-
         static QToolBar *setupSettingsToolBar(MainWindow *p_win, QToolBar *p_toolBar);
 
         static void updateQuickAccessMenu(QMenu *p_menu);

+ 43 - 0
src/widgets/treewidget.cpp

@@ -282,3 +282,46 @@ void TreeWidget::expandRecursively(QTreeWidgetItem *p_item)
         expandRecursively(p_item->child(i));
     }
 }
+
+void TreeWidget::selectParentItem(QTreeWidget *p_widget)
+{
+    auto item = p_widget->currentItem();
+    if (item) {
+        auto pitem = item->parent();
+        if (pitem) {
+            p_widget->setCurrentItem(pitem, 0, QItemSelectionModel::ClearAndSelect);
+        }
+    }
+}
+
+static bool isItemTreeExpanded(const QTreeWidgetItem *p_item)
+{
+    if (!p_item) {
+        return true;
+    }
+
+    if (p_item->isHidden() || !p_item->isExpanded()) {
+        return false;
+    }
+
+    int cnt = p_item->childCount();
+    for (int i = 0; i < cnt; ++i) {
+        if (!isItemTreeExpanded(p_item->child(i))) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool TreeWidget::isExpanded(const QTreeWidget *p_widget)
+{
+    int cnt = p_widget->topLevelItemCount();
+    for (int i = 0; i < cnt; ++i) {
+        if (!isItemTreeExpanded(p_widget->topLevelItem(i))) {
+            return false;
+        }
+    }
+
+    return true;
+}

+ 4 - 0
src/widgets/treewidget.h

@@ -44,6 +44,10 @@ namespace vnotex
 
         static void expandRecursively(QTreeWidgetItem *p_item);
 
+        static void selectParentItem(QTreeWidget *p_widget);
+
+        static bool isExpanded(const QTreeWidget *p_widget);
+
     signals:
         // Emit when single item is selected and Drag&Drop to move internally.
         void itemMoved(QTreeWidgetItem *p_item);

+ 2 - 0
tests/commonfull.pri

@@ -27,3 +27,5 @@ include($$SRC_FOLDER/task/task.pri)
 include($$SRC_FOLDER/core/core.pri)
 
 include($$SRC_FOLDER/widgets/widgets.pri)
+
+include($$SRC_FOLDER/unitedentry/unitedentry.pri)