浏览代码

suport snippet

Le Tan 4 年之前
父节点
当前提交
d1d8fabb60
共有 67 个文件被更改,包括 2015 次插入99 次删除
  1. 1 1
      libs/vtextedit
  2. 8 1
      src/core/configmgr.cpp
  3. 2 0
      src/core/configmgr.h
  4. 1 0
      src/core/coreconfig.h
  5. 2 2
      src/core/sessionconfig.cpp
  6. 2 2
      src/core/sessionconfig.h
  7. 1 0
      src/data/core/vnotex.json
  8. 2 2
      src/data/extra/docs/en/external_programs.md
  9. 2 2
      src/data/extra/docs/zh_CN/external_programs.md
  10. 30 0
      src/snippet/dynamicsnippet.cpp
  11. 30 0
      src/snippet/dynamicsnippet.h
  12. 174 0
      src/snippet/snippet.cpp
  13. 98 0
      src/snippet/snippet.h
  14. 12 0
      src/snippet/snippet.pri
  15. 433 0
      src/snippet/snippetmgr.cpp
  16. 106 0
      src/snippet/snippetmgr.h
  17. 2 0
      src/src.pro
  18. 12 1
      src/utils/fileutils.cpp
  19. 5 0
      src/utils/fileutils.h
  20. 9 0
      src/utils/utils.cpp
  21. 2 0
      src/utils/utils.h
  22. 3 6
      src/widgets/dialogs/folderpropertiesdialog.cpp
  23. 2 3
      src/widgets/dialogs/folderpropertiesdialog.h
  24. 27 0
      src/widgets/dialogs/managenotebooksdialog.cpp
  25. 4 0
      src/widgets/dialogs/managenotebooksdialog.h
  26. 3 6
      src/widgets/dialogs/newfolderdialog.cpp
  27. 2 3
      src/widgets/dialogs/newfolderdialog.h
  28. 3 6
      src/widgets/dialogs/newnotebookdialog.cpp
  29. 2 3
      src/widgets/dialogs/newnotebookdialog.h
  30. 11 12
      src/widgets/dialogs/newnotedialog.cpp
  31. 2 3
      src/widgets/dialogs/newnotedialog.h
  32. 88 0
      src/widgets/dialogs/newsnippetdialog.cpp
  33. 34 0
      src/widgets/dialogs/newsnippetdialog.h
  34. 3 2
      src/widgets/dialogs/nodeinfowidget.cpp
  35. 4 3
      src/widgets/dialogs/nodeinfowidget.h
  36. 3 2
      src/widgets/dialogs/notebookinfowidget.cpp
  37. 2 1
      src/widgets/dialogs/notebookinfowidget.h
  38. 6 11
      src/widgets/dialogs/notepropertiesdialog.cpp
  39. 2 3
      src/widgets/dialogs/notepropertiesdialog.h
  40. 10 0
      src/widgets/dialogs/settings/quickaccesspage.cpp
  41. 173 0
      src/widgets/dialogs/snippetinfowidget.cpp
  42. 73 0
      src/widgets/dialogs/snippetinfowidget.h
  43. 98 0
      src/widgets/dialogs/snippetpropertiesdialog.cpp
  44. 37 0
      src/widgets/dialogs/snippetpropertiesdialog.h
  45. 29 0
      src/widgets/lineeditwithsnippet.cpp
  46. 25 0
      src/widgets/lineeditwithsnippet.h
  47. 4 3
      src/widgets/locationlist.cpp
  48. 55 15
      src/widgets/mainwindow.cpp
  49. 9 1
      src/widgets/mainwindow.h
  50. 14 0
      src/widgets/markdownviewwindow.cpp
  51. 2 0
      src/widgets/markdownviewwindow.h
  52. 1 0
      src/widgets/notebookexplorer.cpp
  53. 1 0
      src/widgets/outlineviewer.cpp
  54. 1 1
      src/widgets/searchpanel.h
  55. 232 0
      src/widgets/snippetpanel.cpp
  56. 53 0
      src/widgets/snippetpanel.h
  57. 14 0
      src/widgets/textviewwindow.cpp
  58. 2 0
      src/widgets/textviewwindow.h
  59. 1 1
      src/widgets/titlebar.cpp
  60. 2 2
      src/widgets/viewarea.h
  61. 10 0
      src/widgets/viewsplit.cpp
  62. 2 0
      src/widgets/viewwindow.h
  63. 4 0
      src/widgets/viewwindowtoolbarhelper.cpp
  64. 10 0
      src/widgets/widgets.pri
  65. 11 1
      src/widgets/widgetsfactory.cpp
  66. 6 0
      src/widgets/widgetsfactory.h
  67. 1 0
      tests/test_core/test_notebook/test_notebook.pro

+ 1 - 1
libs/vtextedit

@@ -1 +1 @@
-Subproject commit ace5699b65e61dfaca1e252364a8a735048e115b
+Subproject commit 98274148a0e1ad371f29abe072fac35bf5d7b6df

+ 8 - 1
src/core/configmgr.cpp

@@ -24,7 +24,7 @@
 using namespace vnotex;
 using namespace vnotex;
 
 
 #ifndef QT_NO_DEBUG
 #ifndef QT_NO_DEBUG
-    #define VX_DEBUG_WEB
+//    #define VX_DEBUG_WEB
 #endif
 #endif
 
 
 const QString ConfigMgr::c_orgName = "VNote";
 const QString ConfigMgr::c_orgName = "VNote";
@@ -388,6 +388,13 @@ QString ConfigMgr::getUserTemplateFolder() const
     return folderPath;
     return folderPath;
 }
 }
 
 
+QString ConfigMgr::getUserSnippetFolder() const
+{
+    auto folderPath = PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("snippets"));
+    QDir().mkpath(folderPath);
+    return folderPath;
+}
+
 QString ConfigMgr::getUserOrAppFile(const QString &p_filePath) const
 QString ConfigMgr::getUserOrAppFile(const QString &p_filePath) const
 {
 {
     QFileInfo fi(p_filePath);
     QFileInfo fi(p_filePath);

+ 2 - 0
src/core/configmgr.h

@@ -93,6 +93,8 @@ namespace vnotex
 
 
         QString getUserTemplateFolder() const;
         QString getUserTemplateFolder() const;
 
 
+        QString getUserSnippetFolder() const;
+
         // If @p_filePath is absolute, just return it.
         // If @p_filePath is absolute, just return it.
         // Otherwise, first try to find it in user folder, then in app folder.
         // Otherwise, first try to find it in user folder, then in app folder.
         QString getUserOrAppFile(const QString &p_filePath) const;
         QString getUserOrAppFile(const QString &p_filePath) const;

+ 1 - 0
src/core/coreconfig.h

@@ -24,6 +24,7 @@ namespace vnotex
             NavigationDock,
             NavigationDock,
             OutlineDock,
             OutlineDock,
             SearchDock,
             SearchDock,
+            SnippetDock,
             LocationListDock,
             LocationListDock,
             Search,
             Search,
             NavigationMode,
             NavigationMode,

+ 2 - 2
src/core/sessionconfig.cpp

@@ -216,7 +216,7 @@ QJsonObject SessionConfig::saveStateAndGeometry() const
     QJsonObject obj;
     QJsonObject obj;
     writeByteArray(obj, QStringLiteral("main_window_state"), m_mainWindowStateGeometry.m_mainState);
     writeByteArray(obj, QStringLiteral("main_window_state"), m_mainWindowStateGeometry.m_mainState);
     writeByteArray(obj, QStringLiteral("main_window_geometry"), m_mainWindowStateGeometry.m_mainGeometry);
     writeByteArray(obj, QStringLiteral("main_window_geometry"), m_mainWindowStateGeometry.m_mainGeometry);
-    writeBitArray(obj, QStringLiteral("docks_visibility_before_expand"), m_mainWindowStateGeometry.m_docksVisibilityBeforeExpand);
+    writeStringList(obj, QStringLiteral("visible_docks_before_expand"), m_mainWindowStateGeometry.m_visibleDocksBeforeExpand);
     return obj;
     return obj;
 }
 }
 
 
@@ -336,7 +336,7 @@ void SessionConfig::loadStateAndGeometry(const QJsonObject &p_session)
     const auto obj = p_session.value(QStringLiteral("state_geometry")).toObject();
     const auto obj = p_session.value(QStringLiteral("state_geometry")).toObject();
     m_mainWindowStateGeometry.m_mainState = readByteArray(obj, QStringLiteral("main_window_state"));
     m_mainWindowStateGeometry.m_mainState = readByteArray(obj, QStringLiteral("main_window_state"));
     m_mainWindowStateGeometry.m_mainGeometry = readByteArray(obj, QStringLiteral("main_window_geometry"));
     m_mainWindowStateGeometry.m_mainGeometry = readByteArray(obj, QStringLiteral("main_window_geometry"));
-    m_mainWindowStateGeometry.m_docksVisibilityBeforeExpand = readBitArray(obj, QStringLiteral("docks_visibility_before_expand"));
+    m_mainWindowStateGeometry.m_visibleDocksBeforeExpand = readStringList(obj, QStringLiteral("visible_docks_before_expand"));
 }
 }
 
 
 QByteArray SessionConfig::getViewAreaSessionAndClear()
 QByteArray SessionConfig::getViewAreaSessionAndClear()

+ 2 - 2
src/core/sessionconfig.h

@@ -35,14 +35,14 @@ namespace vnotex
             {
             {
                 return m_mainState == p_other.m_mainState
                 return m_mainState == p_other.m_mainState
                        && m_mainGeometry == p_other.m_mainGeometry
                        && m_mainGeometry == p_other.m_mainGeometry
-                       && m_docksVisibilityBeforeExpand == p_other.m_docksVisibilityBeforeExpand;
+                       && m_visibleDocksBeforeExpand == p_other.m_visibleDocksBeforeExpand;
             }
             }
 
 
             QByteArray m_mainState;
             QByteArray m_mainState;
 
 
             QByteArray m_mainGeometry;
             QByteArray m_mainGeometry;
 
 
-            QBitArray m_docksVisibilityBeforeExpand;
+            QStringList m_visibleDocksBeforeExpand;
         };
         };
 
 
         enum OpenGL
         enum OpenGL

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

@@ -18,6 +18,7 @@
             "NavigationDock" : "Ctrl+G, A",
             "NavigationDock" : "Ctrl+G, A",
             "OutlineDock" : "Ctrl+G, U",
             "OutlineDock" : "Ctrl+G, U",
             "SearchDock" : "Ctrl+G, S",
             "SearchDock" : "Ctrl+G, S",
+            "SnippetDock" : "",
             "LocationListDock" : "Ctrl+G, L",
             "LocationListDock" : "Ctrl+G, L",
             "Search" : "Ctrl+Alt+F",
             "Search" : "Ctrl+Alt+F",
             "NavigationMode" : "Ctrl+G, W",
             "NavigationMode" : "Ctrl+G, W",

+ 2 - 2
src/data/extra/docs/en/external_programs.md

@@ -1,7 +1,7 @@
 # External Programs
 # External Programs
 VNote allows user to open notes with **external programs** via the `Open With` in the context menu of the node explorer.
 VNote allows user to open notes with **external programs** via the `Open With` in the context menu of the node explorer.
 
 
-To add custom external programs, user needs to edit the session configuration. A sample may look like this:
+To add custom external programs, user needs to edit the session configuration (the `session.json` file in user configuration folder). A sample may look like this:
 
 
 ```json
 ```json
 {
 {
@@ -27,4 +27,4 @@ An external program could have 3 properties:
     1. Use `%1` as a placeholder which will be replaced by the real file paths (automatically wrapped by double quotes);
     1. Use `%1` as a placeholder which will be replaced by the real file paths (automatically wrapped by double quotes);
 3. `shortcut`: the shortcut assigned to this external program;
 3. `shortcut`: the shortcut assigned to this external program;
 
 
-Close VNote before editting the session configuration.
+**Close VNote** before editting the session configuration.

+ 2 - 2
src/data/extra/docs/zh_CN/external_programs.md

@@ -1,7 +1,7 @@
 # 外部程序
 # 外部程序
 VNote 支持通过在节点浏览器上下文菜单中的 `打开方式` 来调用 **外部程序** 打开笔记。
 VNote 支持通过在节点浏览器上下文菜单中的 `打开方式` 来调用 **外部程序** 打开笔记。
 
 
-用户需要编辑会话配置来添加自定义外部程序。一个例子如下:
+用户需要编辑会话配置(用户配置文件夹下的 `session.json` 文件)来添加自定义外部程序。一个例子如下:
 
 
 ```json
 ```json
 {
 {
@@ -27,4 +27,4 @@ VNote 支持通过在节点浏览器上下文菜单中的 `打开方式` 来调
     1. 使用 `%1` 占位符,会被替换为真实的文件路径(自动加上双引号包裹);
     1. 使用 `%1` 占位符,会被替换为真实的文件路径(自动加上双引号包裹);
 3. `shortcut`: 分配给该外部程序的快捷键;
 3. `shortcut`: 分配给该外部程序的快捷键;
 
 
-修改配置前请关闭 VNote。
+修改配置前请 **关闭 VNote** 

+ 30 - 0
src/snippet/dynamicsnippet.cpp

@@ -0,0 +1,30 @@
+#include "dynamicsnippet.h"
+
+#include <QDebug>
+
+using namespace vnotex;
+
+DynamicSnippet::DynamicSnippet(const QString &p_name,
+                               const QString &p_description,
+                               const Callback &p_callback)
+    : Snippet(p_name,
+              p_description,
+              Snippet::InvalidShortcut,
+              false,
+              QString(),
+              QString()),
+      m_callback(p_callback)
+{
+    setType(Type::Dynamic);
+    setReadOnly(true);
+}
+
+QString DynamicSnippet::apply(const QString &p_selectedText,
+                              const QString &p_indentationSpaces,
+                              int &p_cursorOffset)
+{
+    Q_UNUSED(p_indentationSpaces);
+    auto text = m_callback(p_selectedText);
+    p_cursorOffset = text.size();
+    return text;
+}

+ 30 - 0
src/snippet/dynamicsnippet.h

@@ -0,0 +1,30 @@
+#ifndef DYNAMICSNIPPET_H
+#define DYNAMICSNIPPET_H
+
+#include "snippet.h"
+
+#include <functional>
+
+namespace vnotex
+{
+    // Snippet based on function.
+    // To replace the legacy Magic Word.
+    class DynamicSnippet : public Snippet
+    {
+    public:
+        typedef std::function<QString(const QString &)> Callback;
+
+        DynamicSnippet(const QString &p_name,
+                       const QString &p_description,
+                       const Callback &p_callback);
+
+        QString apply(const QString &p_selectedText,
+                      const QString &p_indentationSpaces,
+                      int &p_cursorOffset) Q_DECL_OVERRIDE;
+
+    private:
+        Callback m_callback;
+    };
+}
+
+#endif // DYNAMICSNIPPET_H

+ 174 - 0
src/snippet/snippet.cpp

@@ -0,0 +1,174 @@
+#include "snippet.h"
+
+#include <QDebug>
+
+#include <utils/utils.h>
+
+using namespace vnotex;
+
+const QString Snippet::c_defaultCursorMark = QStringLiteral("@@");
+
+const QString Snippet::c_defaultSelectionMark = QStringLiteral("$$");
+
+Snippet::Snippet(const QString &p_name)
+    : m_name(p_name)
+{
+}
+
+Snippet::Snippet(const QString &p_name,
+                 const QString &p_content,
+                 int p_shortcut,
+                 bool p_indentAsFirstLine,
+                 const QString &p_cursorMark,
+                 const QString &p_selectionMark)
+    : m_type(Type::Text),
+      m_name(p_name),
+      m_content(p_content),
+      m_shortcut(p_shortcut),
+      m_indentAsFirstLine(p_indentAsFirstLine),
+      m_cursorMark(p_cursorMark),
+      m_selectionMark(p_selectionMark)
+{
+}
+
+QJsonObject Snippet::toJson() const
+{
+    QJsonObject obj;
+
+    obj[QStringLiteral("type")] = static_cast<int>(m_type);
+    obj[QStringLiteral("content")] = m_content;
+    obj[QStringLiteral("shortcut")] = m_shortcut;
+    obj[QStringLiteral("indent_as_first_line")] = m_indentAsFirstLine;
+    obj[QStringLiteral("cursor_mark")] = m_cursorMark;
+    obj[QStringLiteral("selection_mark")] = m_selectionMark;
+
+    return obj;
+}
+
+void Snippet::fromJson(const QJsonObject &p_jobj)
+{
+    m_type = static_cast<Type>(p_jobj[QStringLiteral("type")].toInt());
+    m_content = p_jobj[QStringLiteral("content")].toString();
+    m_shortcut = p_jobj[QStringLiteral("shortcut")].toInt();
+    m_indentAsFirstLine = p_jobj[QStringLiteral("indent_as_first_line")].toBool();
+    m_cursorMark = p_jobj[QStringLiteral("cursor_mark")].toString();
+    m_selectionMark = p_jobj[QStringLiteral("selection_mark")].toString();
+}
+
+bool Snippet::isValid() const
+{
+    return !m_name.isEmpty() && m_type != Type::Invalid;
+}
+
+const QString &Snippet::getName() const
+{
+    return m_name;
+}
+
+int Snippet::getShortcut() const
+{
+    return m_shortcut;
+}
+
+QString Snippet::getShortcutString() const
+{
+    if (m_shortcut == InvalidShortcut) {
+        return QString();
+    } else {
+        return Utils::intToString(m_shortcut, 2);
+    }
+}
+
+Snippet::Type Snippet::getType() const
+{
+    return m_type;
+}
+
+const QString &Snippet::getCursorMark() const
+{
+    return m_cursorMark;
+}
+
+const QString &Snippet::getSelectionMark() const
+{
+    return m_selectionMark;
+}
+
+bool Snippet::isIndentAsFirstLineEnabled() const
+{
+    return m_indentAsFirstLine;
+}
+
+const QString &Snippet::getContent() const
+{
+    return m_content;
+}
+
+QString Snippet::apply(const QString &p_selectedText,
+                       const QString &p_indentationSpaces,
+                       int &p_cursorOffset)
+{
+    QString appliedText;
+    p_cursorOffset = 0;
+    if (!isValid() || m_content.isEmpty()) {
+        qWarning() << "failed to apply an invalid snippet" << m_name;
+        return appliedText;
+    }
+
+    // Indent each line after the first line.
+    if (m_indentAsFirstLine && !p_indentationSpaces.isEmpty()) {
+        auto lines = m_content.split(QLatin1Char('\n'));
+        Q_ASSERT(!lines.isEmpty());
+        appliedText = lines[0];
+        for (int i = 1; i < lines.size(); ++i) {
+            appliedText += QLatin1Char('\n') + p_indentationSpaces + lines[i];
+        }
+    } else {
+        appliedText = m_content;
+    }
+
+    // Find the cursor mark and break the content.
+    QString secondPart;
+    if (!m_cursorMark.isEmpty()) {
+        QStringList parts = appliedText.split(m_cursorMark);
+        Q_ASSERT(!parts.isEmpty());
+        if (parts.size() > 2) {
+            qWarning() << "failed to apply snippet with multiple cursor marks" << m_name;
+            return QString();
+        }
+
+        appliedText = parts[0];
+        if (parts.size() == 2) {
+            secondPart = parts[1];
+        }
+    }
+
+    // Replace the selection mark.
+    if (!m_selectionMark.isEmpty()) {
+        if (!appliedText.isEmpty()) {
+            appliedText.replace(m_selectionMark, p_selectedText);
+        }
+
+        if (!secondPart.isEmpty()) {
+            secondPart.replace(m_selectionMark, p_selectedText);
+        }
+    }
+
+    p_cursorOffset = appliedText.size();
+    return appliedText + secondPart;
+}
+
+bool Snippet::isReadOnly() const
+{
+    return m_readOnly;
+}
+
+void Snippet::setReadOnly(bool p_readOnly)
+{
+    m_readOnly = p_readOnly;
+}
+
+void Snippet::setType(Type p_type)
+{
+    m_type = p_type;
+}

+ 98 - 0
src/snippet/snippet.h

@@ -0,0 +1,98 @@
+#ifndef SNIPPET_H
+#define SNIPPET_H
+
+#include <QString>
+#include <QJsonObject>
+
+namespace vnotex
+{
+    class Snippet
+    {
+    public:
+        enum class Type
+        {
+            Invalid,
+            Text,
+            Script,
+            Dynamic
+        };
+
+        enum { InvalidShortcut = -1 };
+
+        Snippet() = default;
+
+        explicit Snippet(const QString &p_name);
+
+        Snippet(const QString &p_name,
+                const QString &p_content,
+                int p_shortcut,
+                bool p_indentAsFirstLine,
+                const QString &p_cursorMark,
+                const QString &p_selectionMark);
+
+        virtual ~Snippet() = default;
+
+        QJsonObject toJson() const;
+        void fromJson(const QJsonObject &p_jobj);
+
+        bool isValid() const;
+
+        bool isReadOnly() const;
+
+        void setReadOnly(bool p_readOnly);
+
+        const QString &getName() const;
+
+        Type getType() const;
+
+        int getShortcut() const;
+
+        QString getShortcutString() const;
+
+        const QString &getCursorMark() const;
+
+        const QString &getSelectionMark() const;
+
+        bool isIndentAsFirstLineEnabled() const;
+
+        const QString &getContent() const;
+
+        // Apply the snippet to generate result text.
+        virtual QString apply(const QString &p_selectedText,
+                              const QString &p_indentationSpaces,
+                              int &p_cursorOffset);
+
+        static const QString c_defaultCursorMark;
+
+        static const QString c_defaultSelectionMark;
+
+    protected:
+        void setType(Type p_type);
+
+    private:
+        bool m_readOnly = false;
+
+        Type m_type = Type::Invalid;
+
+        // Name (and file name) of the snippet.
+        // To avoid mixed with shortcut, the name should not contain digits.
+        QString m_name;
+
+        // Content of the snippet if it is Text.
+        // Embedded snippet is supported.
+        QString m_content;
+
+        // Shortcut digits of this snippet.
+        int m_shortcut = InvalidShortcut;
+
+        bool m_indentAsFirstLine = false;
+
+        // CursorMark is a mark string to indicate the cursor position after applying the snippet.
+        QString m_cursorMark;
+
+        // SelectionMark is a mark string which will be replaced by the selected text before applying the snippet after a snippet is applied.
+        QString m_selectionMark;
+    };
+}
+
+#endif // SNIPPET_H

+ 12 - 0
src/snippet/snippet.pri

@@ -0,0 +1,12 @@
+QT += widgets
+
+HEADERS += \
+    $$PWD/dynamicsnippet.h \
+    $$PWD/snippet.h \
+    $$PWD/snippetmgr.h
+
+SOURCES += \
+    $$PWD/dynamicsnippet.cpp \
+    $$PWD/snippet.cpp \
+    $$PWD/snippetmgr.cpp
+

+ 433 - 0
src/snippet/snippetmgr.cpp

@@ -0,0 +1,433 @@
+#include "snippetmgr.h"
+
+#include <QDir>
+#include <QJsonDocument>
+#include <QDebug>
+#include <QFileInfo>
+#include <QSet>
+#include <QTextCursor>
+#include <QRegularExpression>
+#include <QDateTime>
+
+#include <core/configmgr.h>
+#include <buffer/buffer.h>
+#include <utils/fileutils.h>
+#include <utils/pathutils.h>
+#include <vtextedit/vtextedit.h>
+#include <vtextedit/texteditutils.h>
+#include <vtextedit/textutils.h>
+
+using namespace vnotex;
+
+const QString SnippetMgr::c_snippetSymbolRegExp = QString("%([^%]+)%");
+
+SnippetMgr::SnippetMgr()
+{
+    loadSnippets();
+}
+
+QString SnippetMgr::getSnippetFolder() const
+{
+    return ConfigMgr::getInst().getUserSnippetFolder();
+}
+
+const QVector<QSharedPointer<Snippet>> &SnippetMgr::getSnippets() const
+{
+    return m_snippets;
+}
+
+void SnippetMgr::loadSnippets()
+{
+    Q_ASSERT(m_snippets.isEmpty());
+
+    auto builtInSnippets = loadBuiltInSnippets();
+
+    QSet<QString> names;
+    for (const auto &snippet : builtInSnippets) {
+        Q_ASSERT(!names.contains(snippet->getName()));
+        names.insert(snippet->getName());
+    }
+
+    // Look for all the *.json files.
+    QDir dir(getSnippetFolder());
+    dir.setFilter(QDir::Files | QDir::NoSymLinks);
+    const auto jsonFiles = dir.entryList(QStringList() << "*.json");
+    for (const auto &jsonFile : jsonFiles) {
+        auto snip = loadSnippet(dir.filePath(jsonFile));
+        if (snip->isValid()) {
+            if (names.contains(snip->getName())) {
+                qWarning() << "skip loading snippet with name conflict" << snip->getName() << jsonFile;
+                continue;
+            }
+
+            names.insert(snip->getName());
+
+            addOneSnippet(snip);
+        }
+    }
+
+    m_snippets.append(builtInSnippets);
+
+    qDebug() << "loaded" << m_snippets.size() << "snippets";
+}
+
+QVector<int> SnippetMgr::getAvailableShortcuts(int p_exemption) const
+{
+    QVector<int> shortcuts;
+
+    for (int i = 0; i < 100; ++i) {
+        if (!m_shortcutToSnippet.contains(i) || i == p_exemption) {
+            shortcuts.push_back(i);
+        }
+    }
+
+    return shortcuts;
+}
+
+QSharedPointer<Snippet> SnippetMgr::find(const QString &p_name, Qt::CaseSensitivity p_cs) const
+{
+    if (p_cs == Qt::CaseInsensitive) {
+        const auto lowerName = p_name.toLower();
+        for (const auto &snip : m_snippets) {
+            if (snip->getName().toLower() == lowerName) {
+                return snip;
+            }
+        }
+    } else {
+        for (const auto &snip : m_snippets) {
+            if (snip->getName() == p_name) {
+                return snip;
+            }
+        }
+    }
+
+    return nullptr;
+}
+
+void SnippetMgr::addSnippet(const QSharedPointer<Snippet> &p_snippet)
+{
+    Q_ASSERT(!find(p_snippet->getName(), Qt::CaseInsensitive));
+    saveSnippet(p_snippet);
+    addOneSnippet(p_snippet);
+}
+
+void SnippetMgr::addOneSnippet(const QSharedPointer<Snippet> &p_snippet)
+{
+    m_snippets.push_back(p_snippet);
+    addSnippetToShortcutMap(p_snippet);
+}
+
+QSharedPointer<Snippet> SnippetMgr::loadSnippet(const QString &p_snippetFile) const
+{
+    const auto obj = FileUtils::readJsonFile(p_snippetFile);
+    auto snip = QSharedPointer<Snippet>::create(QFileInfo(p_snippetFile).completeBaseName());
+    snip->fromJson(obj);
+    return snip;
+}
+
+void SnippetMgr::saveSnippet(const QSharedPointer<Snippet> &p_snippet)
+{
+    Q_ASSERT(p_snippet->isValid()
+             && !p_snippet->isReadOnly()
+             && p_snippet->getType() != Snippet::Type::Dynamic);
+    FileUtils::writeFile(getSnippetFile(p_snippet), p_snippet->toJson());
+}
+
+void SnippetMgr::removeSnippet(const QString &p_name)
+{
+    auto snippet = find(p_name);
+    if (!snippet || snippet->isReadOnly()) {
+        return;
+    }
+
+    removeSnippetFromShortcutMap(snippet);
+    m_snippets.removeAll(snippet);
+    FileUtils::removeFile(getSnippetFile(snippet));
+}
+
+QString SnippetMgr::getSnippetFile(const QSharedPointer<Snippet> &p_snippet) const
+{
+    return PathUtils::concatenateFilePath(getSnippetFolder(), p_snippet->getName() + QStringLiteral(".json"));
+}
+
+void SnippetMgr::updateSnippet(const QString &p_name, const QSharedPointer<Snippet> &p_snippet)
+{
+    auto snippet = find(p_name);
+    Q_ASSERT(snippet);
+
+    // If renamed, remove the old file first.
+    if (p_name != p_snippet->getName()) {
+        FileUtils::removeFile(getSnippetFile(snippet));
+    }
+
+    removeSnippetFromShortcutMap(snippet);
+
+    *snippet = *p_snippet;
+    saveSnippet(snippet);
+
+    addSnippetToShortcutMap(snippet);
+}
+
+void SnippetMgr::removeSnippetFromShortcutMap(const QSharedPointer<Snippet> &p_snippet)
+{
+    if (p_snippet->getShortcut() != Snippet::InvalidShortcut) {
+        auto iter = m_shortcutToSnippet.find(p_snippet->getShortcut());
+        Q_ASSERT(iter != m_shortcutToSnippet.end());
+        if (iter.value() == p_snippet) {
+            // There may exist conflict in shortcut.
+            m_shortcutToSnippet.erase(iter);
+        }
+    }
+}
+
+void SnippetMgr::addSnippetToShortcutMap(const QSharedPointer<Snippet> &p_snippet)
+{
+    if (p_snippet->getShortcut() != Snippet::InvalidShortcut) {
+        m_shortcutToSnippet.insert(p_snippet->getShortcut(), p_snippet);
+    }
+}
+
+void SnippetMgr::applySnippet(const QString &p_name,
+                              vte::VTextEdit *p_textEdit,
+                              const OverrideMap &p_overrides) const
+{
+    auto snippet = find(p_name);
+    if (!snippet) {
+        return;
+    }
+    Q_ASSERT(snippet->isValid());
+
+    auto cursor = p_textEdit->textCursor();
+    cursor.beginEditBlock();
+
+    // Get selected text.
+    const auto selectedText = p_textEdit->selectedText();
+    p_textEdit->removeSelectedText();
+
+    QString appliedText;
+    int cursorOffset = 0;
+
+    auto it = p_overrides.find(p_name);
+    if (it != p_overrides.end()) {
+        appliedText = it.value();
+        cursorOffset = appliedText.size();
+    } else {
+        // Fetch indentation of first line.
+        QString indentationSpaces;
+        if (snippet->isIndentAsFirstLineEnabled()) {
+            indentationSpaces = vte::TextEditUtils::fetchIndentationSpaces(cursor.block());
+        }
+
+        appliedText = snippet->apply(selectedText, indentationSpaces, cursorOffset);
+        appliedText = applySnippetBySymbol(appliedText, selectedText, cursorOffset, p_overrides);
+    }
+
+    const int beforePos = cursor.position();
+    cursor.insertText(appliedText);
+    cursor.setPosition(beforePos + cursorOffset);
+
+    cursor.endEditBlock();
+    p_textEdit->setTextCursor(cursor);
+}
+
+QString SnippetMgr::applySnippetBySymbol(const QString &p_content) const
+{
+    int offset = 0;
+    return applySnippetBySymbol(p_content, QString(), offset);
+}
+
+QString SnippetMgr::applySnippetBySymbol(const QString &p_content,
+                                         const QString &p_selectedText,
+                                         int &p_cursorOffset,
+                                         const OverrideMap &p_overrides) const
+{
+    QString content(p_content);
+
+    int maxTimes = 100;
+
+    QRegularExpression regExp(c_snippetSymbolRegExp);
+    int pos = 0;
+    while (pos < content.size() && maxTimes-- > 0) {
+        QRegularExpressionMatch match;
+        int idx = content.indexOf(regExp, pos, &match);
+        if (idx == -1) {
+            break;
+        }
+
+        const auto snippetName = match.captured(1);
+        auto snippet = find(snippetName);
+        if (!snippet) {
+            // Skip it.
+            pos = idx + match.capturedLength(0);
+            continue;
+        }
+
+        QString afterText;
+
+        auto it = p_overrides.find(snippetName);
+        if (it != p_overrides.end()) {
+            afterText = it.value();
+        } else {
+            const auto indentationSpaces = vte::TextUtils::fetchIndentationSpacesInMultiLines(content, idx);
+
+            // Ignore the cursor mark.
+            int ignoredCursorOffset = 0;
+            afterText = snippet->apply(p_selectedText, indentationSpaces, ignoredCursorOffset);
+        }
+
+        content.replace(idx, match.capturedLength(0), afterText);
+
+        // Maintain the cursor offset.
+        if (p_cursorOffset > idx) {
+            if (p_cursorOffset < idx + match.capturedLength(0)) {
+                p_cursorOffset = idx;
+            } else {
+                p_cursorOffset += (afterText.size() - match.capturedLength(0));
+            }
+        }
+
+        // @afterText may still contains snippet symbol.
+        pos = idx;
+    }
+
+    return content;
+}
+
+// Used as the function template for some date/time related dynamic snippets.
+static QString formattedDateTime(const QString &p_format)
+{
+    return QDateTime::currentDateTime().toString(p_format);
+}
+
+QVector<QSharedPointer<Snippet>> SnippetMgr::loadBuiltInSnippets() const
+{
+    QVector<QSharedPointer<Snippet>> snippets;
+
+    addDynamicSnippet(snippets,
+                      "d",
+                      tr("the day as number without a leading zero (`1` to `31`)"),
+                      std::bind(formattedDateTime, "d"));
+    addDynamicSnippet(snippets,
+                      "dd",
+                      tr("the day as number with a leading zero (`01` to `31`)"),
+                      std::bind(formattedDateTime, "dd"));
+    addDynamicSnippet(snippets,
+                      "ddd",
+                      tr("the abbreviated localized day name (e.g. `Mon` to `Sun`)"),
+                      std::bind(formattedDateTime, "ddd"));
+    addDynamicSnippet(snippets,
+                      "dddd",
+                      tr("the long localized day name (e.g. `Monday` to `Sunday`)"),
+                      std::bind(formattedDateTime, "dddd"));
+    addDynamicSnippet(snippets,
+                      "M",
+                      tr("the month as number without a leading zero (`1` to `12`)"),
+                      std::bind(formattedDateTime, "M"));
+    addDynamicSnippet(snippets,
+                      "MM",
+                      tr("the month as number with a leading zero (`01` to `12`)"),
+                      std::bind(formattedDateTime, "MM"));
+    addDynamicSnippet(snippets,
+                      "MMM",
+                      tr("the abbreviated localized month name (e.g. `Jan` to `Dec`)"),
+                      std::bind(formattedDateTime, "MMM"));
+    addDynamicSnippet(snippets,
+                      "MMMM",
+                      tr("the long localized month name (e.g. `January` to `December`)"),
+                      std::bind(formattedDateTime, "MMMM"));
+    addDynamicSnippet(snippets,
+                      "yy",
+                      tr("the year as two digit numbers (`00` to `99`)"),
+                      std::bind(formattedDateTime, "yy"));
+    addDynamicSnippet(snippets,
+                      "yyyy",
+                      tr("the year as four digit numbers"),
+                      std::bind(formattedDateTime, "yyyy"));
+    addDynamicSnippet(snippets,
+                      "w",
+                      tr("the week number (`1` to `53`)"),
+                      [](const QString &) {
+                          return QString::number(QDate::currentDate().weekNumber());
+                      });
+    addDynamicSnippet(snippets,
+                      "H",
+                      tr("the hour without a leading zero (`0` to `23` even with AM/PM display)"),
+                      std::bind(formattedDateTime, "H"));
+    addDynamicSnippet(snippets,
+                      "HH",
+                      tr("the hour with a leading zero (`00` to `23` even with AM/PM display)"),
+                      std::bind(formattedDateTime, "HH"));
+    addDynamicSnippet(snippets,
+                      "m",
+                      tr("the minute without a leading zero (`0` to `59`)"),
+                      std::bind(formattedDateTime, "m"));
+    addDynamicSnippet(snippets,
+                      "mm",
+                      tr("the minute with a leading zero (`00` to `59`)"),
+                      std::bind(formattedDateTime, "mm"));
+    addDynamicSnippet(snippets,
+                      "s",
+                      tr("the second without a leading zero (`0` to `59`)"),
+                      std::bind(formattedDateTime, "s"));
+    addDynamicSnippet(snippets,
+                      "ss",
+                      tr("the second with a leading zero (`00` to `59`)"),
+                      std::bind(formattedDateTime, "ss"));
+    addDynamicSnippet(snippets,
+                      "date",
+                      tr("date (`2021-02-24`)"),
+                      std::bind(formattedDateTime, "yyyy-MM-dd"));
+    addDynamicSnippet(snippets,
+                      "da",
+                      tr("the abbreviated date (`20210224`)"),
+                      std::bind(formattedDateTime, "yyyyMMdd"));
+    addDynamicSnippet(snippets,
+                      "time",
+                      tr("time (`16:51:02`)"),
+                      std::bind(formattedDateTime, "hh:mm:ss"));
+    addDynamicSnippet(snippets,
+                      "datetime",
+                      tr("date and time (`2021-02-24_16:51:02`)"),
+                      std::bind(formattedDateTime, "yyyy-MM-dd_hh:mm:ss"));
+
+    // These snippets need override to fill the real value.
+    // Check generateOverrides().
+    addDynamicSnippet(snippets,
+                      QStringLiteral("note"),
+                      tr("name of current note"),
+                      [](const QString &) {
+                          return tr("[Value Not Available]");
+                      });
+    addDynamicSnippet(snippets,
+                      QStringLiteral("no"),
+                      tr("complete base name of current note"),
+                      [](const QString &) {
+                          return tr("[Value Not Available]");
+                      });
+    return snippets;
+}
+
+void SnippetMgr::addDynamicSnippet(QVector<QSharedPointer<Snippet>> &p_snippets,
+                                   const QString &p_name,
+                                   const QString &p_description,
+                                   const DynamicSnippet::Callback &p_callback)
+{
+    auto snippet = QSharedPointer<DynamicSnippet>::create(p_name, p_description, p_callback);
+    p_snippets.push_back(snippet);
+}
+
+SnippetMgr::OverrideMap SnippetMgr::generateOverrides(const Buffer *p_buffer)
+{
+    OverrideMap overrides;
+    overrides.insert(QStringLiteral("note"), p_buffer->getName());
+    overrides.insert(QStringLiteral("no"), QFileInfo(p_buffer->getName()).completeBaseName());
+    return overrides;
+}
+
+SnippetMgr::OverrideMap SnippetMgr::generateOverrides(const QString &p_fileName)
+{
+    OverrideMap overrides;
+    overrides.insert(QStringLiteral("note"), p_fileName);
+    overrides.insert(QStringLiteral("no"), QFileInfo(p_fileName).completeBaseName());
+    return overrides;
+}

+ 106 - 0
src/snippet/snippetmgr.h

@@ -0,0 +1,106 @@
+#ifndef SNIPPETMGR_H
+#define SNIPPETMGR_H
+
+#include <QObject>
+#include <QVector>
+#include <QMap>
+#include <QSharedPointer>
+
+#include <core/noncopyable.h>
+
+#include "dynamicsnippet.h"
+
+namespace vte
+{
+    class VTextEdit;
+}
+
+namespace vnotex
+{
+    class Buffer;
+
+    class SnippetMgr : public QObject, private Noncopyable
+    {
+        Q_OBJECT
+    public:
+        typedef QMap<QString, QString> OverrideMap;
+
+        static SnippetMgr &getInst()
+        {
+            static SnippetMgr inst;
+            return inst;
+        }
+
+        QString getSnippetFolder() const;
+
+        const QVector<QSharedPointer<Snippet>> &getSnippets() const;
+
+        // @p_exemption: include it even it is occupied by one snippet.
+        QVector<int> getAvailableShortcuts(int p_exemption = Snippet::InvalidShortcut) const;
+
+        QSharedPointer<Snippet> find(const QString &p_name, Qt::CaseSensitivity p_cs = Qt::CaseSensitive) const;
+
+        void addSnippet(const QSharedPointer<Snippet> &p_snippet);
+
+        void removeSnippet(const QString &p_name);
+
+        void updateSnippet(const QString &p_name, const QSharedPointer<Snippet> &p_snippet);
+
+        // Apply snippet @p_name directly in current cursor position.
+        // For snippets in @p_overrides, we just provide simple contents without nested snippets.
+        void applySnippet(const QString &p_name,
+                          vte::VTextEdit *p_textEdit,
+                          const OverrideMap &p_overrides = OverrideMap()) const;
+
+        // Resolve %snippet_name% as snippet and apply recursively.
+        // Will update @p_cursorOffset if needed.
+        // For snippets in @p_overrides, we just provide simple contents without nested snippets.
+        QString applySnippetBySymbol(const QString &p_content,
+                                     const QString &p_selectedText,
+                                     int &p_cursorOffset,
+                                     const OverrideMap &p_overrides = OverrideMap()) const;
+
+        QString applySnippetBySymbol(const QString &p_content) const;
+
+        // Generate standard overrides for given buffer.
+        static OverrideMap generateOverrides(const Buffer *p_buffer);
+
+        // Generate standard overrides.
+        static OverrideMap generateOverrides(const QString &p_fileName);
+
+        // %name%.
+        // Captured texts:
+        // 1 - The name of the snippet.
+        static const QString c_snippetSymbolRegExp;
+
+    private:
+        SnippetMgr();
+
+        void loadSnippets();
+
+        QSharedPointer<Snippet> loadSnippet(const QString &p_snippetFile) const;
+
+        void saveSnippet(const QSharedPointer<Snippet> &p_snippet);
+
+        QString getSnippetFile(const QSharedPointer<Snippet> &p_snippet) const;
+
+        void addOneSnippet(const QSharedPointer<Snippet> &p_snippet);
+
+        void removeSnippetFromShortcutMap(const QSharedPointer<Snippet> &p_snippet);
+
+        void addSnippetToShortcutMap(const QSharedPointer<Snippet> &p_snippet);
+
+        QVector<QSharedPointer<Snippet>> loadBuiltInSnippets() const;
+
+        static void addDynamicSnippet(QVector<QSharedPointer<Snippet>> &p_snippets,
+                                      const QString &p_name,
+                                      const QString &p_description,
+                                      const DynamicSnippet::Callback &p_callback);
+
+        QVector<QSharedPointer<Snippet>> m_snippets;
+
+        QMap<int, QSharedPointer<Snippet>> m_shortcutToSnippet;
+    };
+}
+
+#endif // SNIPPETMGR_H

+ 2 - 0
src/src.pro

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

+ 12 - 1
src/utils/fileutils.cpp

@@ -4,6 +4,7 @@
 #include <QMimeDatabase>
 #include <QMimeDatabase>
 #include <QDateTime>
 #include <QDateTime>
 #include <QTemporaryFile>
 #include <QTemporaryFile>
+#include <QJsonDocument>
 
 
 #include "../core/exception.h"
 #include "../core/exception.h"
 #include "pathutils.h"
 #include "pathutils.h"
@@ -34,6 +35,11 @@ QString FileUtils::readTextFile(const QString &p_filePath)
     return text;
     return text;
 }
 }
 
 
+QJsonObject FileUtils::readJsonFile(const QString &p_filePath)
+{
+    return QJsonDocument::fromJson(readFile(p_filePath)).object();
+}
+
 void FileUtils::writeFile(const QString &p_filePath, const QByteArray &p_data)
 void FileUtils::writeFile(const QString &p_filePath, const QByteArray &p_data)
 {
 {
     QFile file(p_filePath);
     QFile file(p_filePath);
@@ -59,6 +65,11 @@ void FileUtils::writeFile(const QString &p_filePath, const QString &p_text)
     file.close();
     file.close();
 }
 }
 
 
+void FileUtils::writeFile(const QString &p_filePath, const QJsonObject &p_jobj)
+{
+    writeFile(p_filePath, QJsonDocument(p_jobj).toJson());
+}
+
 void FileUtils::renameFile(const QString &p_path, const QString &p_name)
 void FileUtils::renameFile(const QString &p_path, const QString &p_name)
 {
 {
     Q_ASSERT(PathUtils::isLegalFileName(p_name));
     Q_ASSERT(PathUtils::isLegalFileName(p_name));
@@ -194,7 +205,7 @@ QString FileUtils::renameIfExistsCaseInsensitive(const QString &p_path)
 
 
 void FileUtils::removeFile(const QString &p_filePath)
 void FileUtils::removeFile(const QString &p_filePath)
 {
 {
-    Q_ASSERT(QFileInfo(p_filePath).isFile());
+    Q_ASSERT(!QFileInfo::exists(p_filePath) || QFileInfo(p_filePath).isFile());
     QFile file(p_filePath);
     QFile file(p_filePath);
     if (!file.remove()) {
     if (!file.remove()) {
         Exception::throwOne(Exception::Type::FailToRemoveFile,
         Exception::throwOne(Exception::Type::FailToRemoveFile,

+ 5 - 0
src/utils/fileutils.h

@@ -5,6 +5,7 @@
 #include <QString>
 #include <QString>
 #include <QImage>
 #include <QImage>
 #include <QPixmap>
 #include <QPixmap>
+#include <QJsonObject>
 
 
 class QTemporaryFile;
 class QTemporaryFile;
 
 
@@ -19,10 +20,14 @@ namespace vnotex
 
 
         static QString readTextFile(const QString &p_filePath);
         static QString readTextFile(const QString &p_filePath);
 
 
+        static QJsonObject readJsonFile(const QString &p_filePath);
+
         static void writeFile(const QString &p_filePath, const QByteArray &p_data);
         static void writeFile(const QString &p_filePath, const QByteArray &p_data);
 
 
         static void writeFile(const QString &p_filePath, const QString &p_text);
         static void writeFile(const QString &p_filePath, const QString &p_text);
 
 
+        static void writeFile(const QString &p_filePath, const QJsonObject &p_jobj);
+
         // Rename file or dir.
         // Rename file or dir.
         static void renameFile(const QString &p_path, const QString &p_name);
         static void renameFile(const QString &p_path, const QString &p_name);
 
 

+ 9 - 0
src/utils/utils.cpp

@@ -117,3 +117,12 @@ QString Utils::boolToString(bool p_val)
 {
 {
     return p_val ? QStringLiteral("true") : QStringLiteral("false");
     return p_val ? QStringLiteral("true") : QStringLiteral("false");
 }
 }
+
+QString Utils::intToString(int p_val, int p_width)
+{
+    auto str = QString::number(p_val);
+    if (str.size() < p_width) {
+        str.prepend(QString(p_width - str.size(), QLatin1Char('0')));
+    }
+    return str;
+}

+ 2 - 0
src/utils/utils.h

@@ -50,6 +50,8 @@ namespace vnotex
         static bool fuzzyEqual(qreal p_a, qreal p_b);
         static bool fuzzyEqual(qreal p_a, qreal p_b);
 
 
         static QString boolToString(bool p_val);
         static QString boolToString(bool p_val);
+
+        static QString intToString(int p_val, int p_width = 0);
     };
     };
 } // ns vnotex
 } // ns vnotex
 
 

+ 3 - 6
src/widgets/dialogs/folderpropertiesdialog.cpp

@@ -30,7 +30,6 @@ void FolderPropertiesDialog::setupUI()
     setCentralWidget(m_infoWidget);
     setCentralWidget(m_infoWidget);
 
 
     setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
     setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
-    setButtonEnabled(QDialogButtonBox::Ok, false);
 
 
     setWindowTitle(m_node->getName() + QStringLiteral(" ") + tr("Properties"));
     setWindowTitle(m_node->getName() + QStringLiteral(" ") + tr("Properties"));
 }
 }
@@ -38,11 +37,9 @@ void FolderPropertiesDialog::setupUI()
 void FolderPropertiesDialog::setupNodeInfoWidget(QWidget *p_parent)
 void FolderPropertiesDialog::setupNodeInfoWidget(QWidget *p_parent)
 {
 {
     m_infoWidget = new NodeInfoWidget(m_node, p_parent);
     m_infoWidget = new NodeInfoWidget(m_node, p_parent);
-    connect(m_infoWidget, &NodeInfoWidget::inputEdited,
-            this, &FolderPropertiesDialog::validateInputs);
 }
 }
 
 
-void FolderPropertiesDialog::validateInputs()
+bool FolderPropertiesDialog::validateInputs()
 {
 {
     bool valid = true;
     bool valid = true;
     QString msg;
     QString msg;
@@ -50,7 +47,7 @@ void FolderPropertiesDialog::validateInputs()
     valid = valid && validateNameInput(msg);
     valid = valid && validateNameInput(msg);
     setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
     setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
                                   : ScrollDialog::InformationLevel::Error);
                                   : ScrollDialog::InformationLevel::Error);
-    setButtonEnabled(QDialogButtonBox::Ok, valid);
+    return valid;
 }
 }
 
 
 bool FolderPropertiesDialog::validateNameInput(QString &p_msg)
 bool FolderPropertiesDialog::validateNameInput(QString &p_msg)
@@ -74,7 +71,7 @@ bool FolderPropertiesDialog::validateNameInput(QString &p_msg)
 
 
 void FolderPropertiesDialog::acceptedButtonClicked()
 void FolderPropertiesDialog::acceptedButtonClicked()
 {
 {
-    if (saveFolderProperties()) {
+    if (validateInputs() && saveFolderProperties()) {
         accept();
         accept();
     }
     }
 }
 }

+ 2 - 3
src/widgets/dialogs/folderpropertiesdialog.h

@@ -17,9 +17,6 @@ namespace vnotex
     protected:
     protected:
         void acceptedButtonClicked() Q_DECL_OVERRIDE;
         void acceptedButtonClicked() Q_DECL_OVERRIDE;
 
 
-    private slots:
-        void validateInputs();
-
     private:
     private:
         void setupUI();
         void setupUI();
 
 
@@ -29,6 +26,8 @@ namespace vnotex
 
 
         bool saveFolderProperties();
         bool saveFolderProperties();
 
 
+        bool validateInputs();
+
         NodeInfoWidget *m_infoWidget = nullptr;
         NodeInfoWidget *m_infoWidget = nullptr;
 
 
         Node *m_node = nullptr;
         Node *m_node = nullptr;

+ 27 - 0
src/widgets/dialogs/managenotebooksdialog.cpp

@@ -14,6 +14,7 @@
 #include "notebookmgr.h"
 #include "notebookmgr.h"
 #include "../messageboxhelper.h"
 #include "../messageboxhelper.h"
 #include <utils/iconutils.h>
 #include <utils/iconutils.h>
+#include <utils/utils.h>
 #include <utils/widgetutils.h>
 #include <utils/widgetutils.h>
 #include "../widgetsfactory.h"
 #include "../widgetsfactory.h"
 #include "exception.h"
 #include "exception.h"
@@ -212,12 +213,38 @@ void ManageNotebooksDialog::setChangesUnsaved(bool p_unsaved)
     setButtonEnabled(QDialogButtonBox::Reset, m_changesUnsaved);
     setButtonEnabled(QDialogButtonBox::Reset, m_changesUnsaved);
 }
 }
 
 
+bool ManageNotebooksDialog::validateInputs()
+{
+    bool valid = true;
+    QString msg;
+
+    valid = valid && validateNameInput(msg);
+
+    setInformationText(msg, valid ? Dialog::InformationLevel::Info
+                                  : Dialog::InformationLevel::Error);
+    return valid;
+}
+
+bool ManageNotebooksDialog::validateNameInput(QString &p_msg)
+{
+    if (m_notebookInfoWidget->getName().isEmpty()) {
+        Utils::appendMsg(p_msg, tr("Please specify a name for the notebook."));
+        return false;
+    }
+
+    return true;
+}
+
 bool ManageNotebooksDialog::saveChangesToNotebook()
 bool ManageNotebooksDialog::saveChangesToNotebook()
 {
 {
     if (!m_changesUnsaved || !m_notebook) {
     if (!m_changesUnsaved || !m_notebook) {
         return true;
         return true;
     }
     }
 
 
+    if (!validateInputs()) {
+        return false;
+    }
+
     m_notebook->updateName(m_notebookInfoWidget->getName());
     m_notebook->updateName(m_notebookInfoWidget->getName());
     m_notebook->updateDescription(m_notebookInfoWidget->getDescription());
     m_notebook->updateDescription(m_notebookInfoWidget->getDescription());
     return true;
     return true;

+ 4 - 0
src/widgets/dialogs/managenotebooksdialog.h

@@ -51,6 +51,10 @@ namespace vnotex
 
 
         bool checkUnsavedChanges();
         bool checkUnsavedChanges();
 
 
+        bool validateInputs();
+
+        bool validateNameInput(QString &p_msg);
+
         QListWidget *m_notebookList = nullptr;
         QListWidget *m_notebookList = nullptr;
 
 
         NotebookInfoWidget *m_notebookInfoWidget = nullptr;
         NotebookInfoWidget *m_notebookInfoWidget = nullptr;

+ 3 - 6
src/widgets/dialogs/newfolderdialog.cpp

@@ -26,7 +26,6 @@ void NewFolderDialog::setupUI(const Node *p_node)
     setCentralWidget(m_infoWidget);
     setCentralWidget(m_infoWidget);
 
 
     setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
     setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
-    setButtonEnabled(QDialogButtonBox::Ok, false);
 
 
     setWindowTitle(tr("New Folder"));
     setWindowTitle(tr("New Folder"));
 }
 }
@@ -34,11 +33,9 @@ void NewFolderDialog::setupUI(const Node *p_node)
 void NewFolderDialog::setupNodeInfoWidget(const Node *p_node, QWidget *p_parent)
 void NewFolderDialog::setupNodeInfoWidget(const Node *p_node, QWidget *p_parent)
 {
 {
     m_infoWidget = new NodeInfoWidget(p_node, Node::Flag::Container, p_parent);
     m_infoWidget = new NodeInfoWidget(p_node, Node::Flag::Container, p_parent);
-    connect(m_infoWidget, &NodeInfoWidget::inputEdited,
-            this, &NewFolderDialog::validateInputs);
 }
 }
 
 
-void NewFolderDialog::validateInputs()
+bool NewFolderDialog::validateInputs()
 {
 {
     bool valid = true;
     bool valid = true;
     QString msg;
     QString msg;
@@ -46,7 +43,7 @@ void NewFolderDialog::validateInputs()
     valid = valid && validateNameInput(msg);
     valid = valid && validateNameInput(msg);
     setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
     setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
                                   : ScrollDialog::InformationLevel::Error);
                                   : ScrollDialog::InformationLevel::Error);
-    setButtonEnabled(QDialogButtonBox::Ok, valid);
+    return valid;
 }
 }
 
 
 bool NewFolderDialog::validateNameInput(QString &p_msg)
 bool NewFolderDialog::validateNameInput(QString &p_msg)
@@ -69,7 +66,7 @@ bool NewFolderDialog::validateNameInput(QString &p_msg)
 
 
 void NewFolderDialog::acceptedButtonClicked()
 void NewFolderDialog::acceptedButtonClicked()
 {
 {
-    if (newFolder()) {
+    if (validateInputs() && newFolder()) {
         accept();
         accept();
     }
     }
 }
 }

+ 2 - 3
src/widgets/dialogs/newfolderdialog.h

@@ -20,9 +20,6 @@ namespace vnotex
     protected:
     protected:
         void acceptedButtonClicked() Q_DECL_OVERRIDE;
         void acceptedButtonClicked() Q_DECL_OVERRIDE;
 
 
-    private slots:
-        void validateInputs();
-
     private:
     private:
         void setupUI(const Node *p_node);
         void setupUI(const Node *p_node);
 
 
@@ -32,6 +29,8 @@ namespace vnotex
 
 
         bool newFolder();
         bool newFolder();
 
 
+        bool validateInputs();
+
         NodeInfoWidget *m_infoWidget = nullptr;
         NodeInfoWidget *m_infoWidget = nullptr;
 
 
         QSharedPointer<Node> m_newNode;
         QSharedPointer<Node> m_newNode;

+ 3 - 6
src/widgets/dialogs/newnotebookdialog.cpp

@@ -29,7 +29,6 @@ void NewNotebookDialog::setupUI()
     setCentralWidget(m_infoWidget);
     setCentralWidget(m_infoWidget);
 
 
     setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
     setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
-    setButtonEnabled(QDialogButtonBox::Ok, false);
 
 
     setWindowTitle(tr("New Notebook"));
     setWindowTitle(tr("New Notebook"));
 }
 }
@@ -39,8 +38,6 @@ void NewNotebookDialog::setupNotebookInfoWidget(QWidget *p_parent)
     m_infoWidget = new NotebookInfoWidget(NotebookInfoWidget::Create, p_parent);
     m_infoWidget = new NotebookInfoWidget(NotebookInfoWidget::Create, p_parent);
     connect(m_infoWidget, &NotebookInfoWidget::rootFolderEdited,
     connect(m_infoWidget, &NotebookInfoWidget::rootFolderEdited,
             this, &NewNotebookDialog::handleRootFolderPathChanged);
             this, &NewNotebookDialog::handleRootFolderPathChanged);
-    connect(m_infoWidget, &NotebookInfoWidget::basicInfoEdited,
-            this, &NewNotebookDialog::validateInputs);
 
 
     {
     {
         auto whatsThis = tr("<br/>Both absolute and relative paths are supported. ~ and environment variable are not supported now.");
         auto whatsThis = tr("<br/>Both absolute and relative paths are supported. ~ and environment variable are not supported now.");
@@ -49,7 +46,7 @@ void NewNotebookDialog::setupNotebookInfoWidget(QWidget *p_parent)
     }
     }
 }
 }
 
 
-void NewNotebookDialog::validateInputs()
+bool NewNotebookDialog::validateInputs()
 {
 {
     bool valid = true;
     bool valid = true;
     QString msg;
     QString msg;
@@ -59,7 +56,7 @@ void NewNotebookDialog::validateInputs()
 
 
     setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
     setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
                                   : ScrollDialog::InformationLevel::Error);
                                   : ScrollDialog::InformationLevel::Error);
-    setButtonEnabled(QDialogButtonBox::Ok, valid);
+    return valid;
 }
 }
 
 
 bool NewNotebookDialog::validateNameInput(QString &p_msg)
 bool NewNotebookDialog::validateNameInput(QString &p_msg)
@@ -113,7 +110,7 @@ bool NewNotebookDialog::validateRootFolderInput(QString &p_msg)
 
 
 void NewNotebookDialog::acceptedButtonClicked()
 void NewNotebookDialog::acceptedButtonClicked()
 {
 {
-    if (newNotebook()) {
+    if (validateInputs() && newNotebook()) {
         accept();
         accept();
     }
     }
 }
 }

+ 2 - 3
src/widgets/dialogs/newnotebookdialog.h

@@ -22,14 +22,13 @@ namespace vnotex
 
 
         NotebookInfoWidget *m_infoWidget = nullptr;
         NotebookInfoWidget *m_infoWidget = nullptr;
 
 
-    private slots:
-        void validateInputs();
-
     private:
     private:
         void setupUI();
         void setupUI();
 
 
         void setupNotebookInfoWidget(QWidget *p_parent = nullptr);
         void setupNotebookInfoWidget(QWidget *p_parent = nullptr);
 
 
+        bool validateInputs();
+
         bool validateNameInput(QString &p_msg);
         bool validateNameInput(QString &p_msg);
 
 
         // Create a new notebook.
         // Create a new notebook.

+ 11 - 12
src/widgets/dialogs/newnotedialog.cpp

@@ -16,6 +16,7 @@
 #include "nodeinfowidget.h"
 #include "nodeinfowidget.h"
 #include <utils/widgetutils.h>
 #include <utils/widgetutils.h>
 #include <core/templatemgr.h>
 #include <core/templatemgr.h>
+#include <snippet/snippetmgr.h>
 
 
 using namespace vnotex;
 using namespace vnotex;
 
 
@@ -62,7 +63,6 @@ void NewNoteDialog::setupUI(const Node *p_node)
     }
     }
 
 
     setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
     setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
-    setButtonEnabled(QDialogButtonBox::Ok, false);
 
 
     setWindowTitle(tr("New Note"));
     setWindowTitle(tr("New Note"));
 }
 }
@@ -70,11 +70,9 @@ void NewNoteDialog::setupUI(const Node *p_node)
 void NewNoteDialog::setupNodeInfoWidget(const Node *p_node, QWidget *p_parent)
 void NewNoteDialog::setupNodeInfoWidget(const Node *p_node, QWidget *p_parent)
 {
 {
     m_infoWidget = new NodeInfoWidget(p_node, Node::Flag::Content, p_parent);
     m_infoWidget = new NodeInfoWidget(p_node, Node::Flag::Content, p_parent);
-    connect(m_infoWidget, &NodeInfoWidget::inputEdited,
-            this, &NewNoteDialog::validateInputs);
 }
 }
 
 
-void NewNoteDialog::validateInputs()
+bool NewNoteDialog::validateInputs()
 {
 {
     bool valid = true;
     bool valid = true;
     QString msg;
     QString msg;
@@ -82,7 +80,7 @@ void NewNoteDialog::validateInputs()
     valid = valid && validateNameInput(msg);
     valid = valid && validateNameInput(msg);
     setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
     setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
                                   : ScrollDialog::InformationLevel::Error);
                                   : ScrollDialog::InformationLevel::Error);
-    setButtonEnabled(QDialogButtonBox::Ok, valid);
+    return valid;
 }
 }
 
 
 bool NewNoteDialog::validateNameInput(QString &p_msg)
 bool NewNoteDialog::validateNameInput(QString &p_msg)
@@ -90,8 +88,8 @@ bool NewNoteDialog::validateNameInput(QString &p_msg)
     p_msg.clear();
     p_msg.clear();
 
 
     auto name = m_infoWidget->getName();
     auto name = m_infoWidget->getName();
-    if (name.isEmpty()) {
-        p_msg = tr("Please specify a name for the note.");
+    if (name.isEmpty() || !PathUtils::isLegalFileName(name)) {
+        p_msg = tr("Please specify a valid name for the note.");
         return false;
         return false;
     }
     }
 
 
@@ -107,7 +105,7 @@ void NewNoteDialog::acceptedButtonClicked()
 {
 {
     s_lastTemplate = m_templateComboBox->currentData().toString();
     s_lastTemplate = m_templateComboBox->currentData().toString();
 
 
-    if (newNote()) {
+    if (validateInputs() && newNote()) {
         accept();
         accept();
     }
     }
 }
 }
@@ -151,8 +149,6 @@ void NewNoteDialog::initDefaultValues(const Node *p_node)
                                                                    QStringLiteral("md"));
                                                                    QStringLiteral("md"));
         lineEdit->setText(defaultName);
         lineEdit->setText(defaultName);
         WidgetUtils::selectBaseName(lineEdit);
         WidgetUtils::selectBaseName(lineEdit);
-
-        validateInputs();
     }
     }
 }
 }
 
 
@@ -209,6 +205,9 @@ void NewNoteDialog::setupTemplateComboBox(QWidget *p_parent)
 
 
 QString NewNoteDialog::getTemplateContent() const
 QString NewNoteDialog::getTemplateContent() const
 {
 {
-    // TODO: parse snippets of the template.
-    return m_templateContent;
+    int cursorOffset = 0;
+    return SnippetMgr::getInst().applySnippetBySymbol(m_templateContent,
+                                                      QString(),
+                                                      cursorOffset,
+                                                      SnippetMgr::generateOverrides(m_infoWidget->getName()));
 }
 }

+ 2 - 3
src/widgets/dialogs/newnotedialog.h

@@ -24,9 +24,6 @@ namespace vnotex
     protected:
     protected:
         void acceptedButtonClicked() Q_DECL_OVERRIDE;
         void acceptedButtonClicked() Q_DECL_OVERRIDE;
 
 
-    private slots:
-        void validateInputs();
-
     private:
     private:
         void setupUI(const Node *p_node);
         void setupUI(const Node *p_node);
 
 
@@ -34,6 +31,8 @@ namespace vnotex
 
 
         void setupTemplateComboBox(QWidget *p_parent);
         void setupTemplateComboBox(QWidget *p_parent);
 
 
+        bool validateInputs();
+
         bool validateNameInput(QString &p_msg);
         bool validateNameInput(QString &p_msg);
 
 
         bool newNote();
         bool newNote();

+ 88 - 0
src/widgets/dialogs/newsnippetdialog.cpp

@@ -0,0 +1,88 @@
+#include "newsnippetdialog.h"
+
+#include "snippetinfowidget.h"
+
+#include <snippet/snippetmgr.h>
+#include <core/exception.h>
+
+using namespace vnotex;
+
+NewSnippetDialog::NewSnippetDialog(QWidget *p_parent)
+    : ScrollDialog(p_parent)
+{
+    setupUI();
+
+    m_infoWidget->setFocus();
+}
+
+void NewSnippetDialog::setupUI()
+{
+    setupSnippetInfoWidget(this);
+    setCentralWidget(m_infoWidget);
+
+    setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+
+    setWindowTitle(tr("New Snippet"));
+}
+
+void NewSnippetDialog::setupSnippetInfoWidget(QWidget *p_parent)
+{
+    m_infoWidget = new SnippetInfoWidget(p_parent);
+}
+
+bool NewSnippetDialog::validateInputs()
+{
+    bool valid = true;
+    QString msg;
+
+    valid = valid && validateNameInput(msg);
+    setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
+                                  : ScrollDialog::InformationLevel::Error);
+    return valid;
+}
+
+void NewSnippetDialog::acceptedButtonClicked()
+{
+    if (validateInputs() && newSnippet()) {
+        accept();
+    }
+}
+
+bool NewSnippetDialog::newSnippet()
+{
+    auto snip = QSharedPointer<Snippet>::create(m_infoWidget->getName(),
+                                                m_infoWidget->getContent(),
+                                                m_infoWidget->getShortcut(),
+                                                m_infoWidget->shouldIndentAsFirstLine(),
+                                                m_infoWidget->getCursorMark(),
+                                                m_infoWidget->getSelectionMark());
+    Q_ASSERT(snip->isValid());
+    try {
+        SnippetMgr::getInst().addSnippet(snip);
+    } catch (Exception &p_e) {
+        QString msg = tr("Failed to add snippet (%1) (%2).")
+                        .arg(snip->getName(), p_e.what());
+        qWarning() << msg;
+        setInformationText(msg, ScrollDialog::InformationLevel::Error);
+        return false;
+    }
+    return true;
+}
+
+bool NewSnippetDialog::validateNameInput(QString &p_msg)
+{
+    p_msg.clear();
+
+    const auto name = m_infoWidget->getName();
+    if (name.isEmpty()) {
+        p_msg = tr("Please specify a name for the snippet.");
+        return false;
+    }
+
+    if (SnippetMgr::getInst().find(name, Qt::CaseInsensitive)) {
+        p_msg = tr("Name conflicts with existing snippet.");
+        return false;
+    }
+
+    return true;
+}

+ 34 - 0
src/widgets/dialogs/newsnippetdialog.h

@@ -0,0 +1,34 @@
+#ifndef NEWSNIPPETDIALOG_H
+#define NEWSNIPPETDIALOG_H
+
+#include "scrolldialog.h"
+
+namespace vnotex
+{
+    class SnippetInfoWidget;
+
+    class NewSnippetDialog : public ScrollDialog
+    {
+        Q_OBJECT
+    public:
+        explicit NewSnippetDialog(QWidget *p_parent = nullptr);
+
+    protected:
+        void acceptedButtonClicked() Q_DECL_OVERRIDE;
+
+    private:
+        void setupUI();
+
+        void setupSnippetInfoWidget(QWidget *p_parent);
+
+        bool newSnippet();
+
+        bool validateNameInput(QString &p_msg);
+
+        bool validateInputs();
+
+        SnippetInfoWidget *m_infoWidget = nullptr;
+    };
+}
+
+#endif // NEWSNIPPETDIALOG_H

+ 3 - 2
src/widgets/dialogs/nodeinfowidget.cpp

@@ -13,6 +13,7 @@
 #include "nodelabelwithupbutton.h"
 #include "nodelabelwithupbutton.h"
 #include <utils/widgetutils.h>
 #include <utils/widgetutils.h>
 #include <buffer/filetypehelper.h>
 #include <buffer/filetypehelper.h>
+#include "../lineeditwithsnippet.h"
 
 
 using namespace vnotex;
 using namespace vnotex;
 
 
@@ -69,7 +70,7 @@ void NodeInfoWidget::setupUI(const Node *p_parentNode, Node::Flags p_newNodeFlag
 
 
 void NodeInfoWidget::setupNameLineEdit(QWidget *p_parent)
 void NodeInfoWidget::setupNameLineEdit(QWidget *p_parent)
 {
 {
-    m_nameLineEdit = WidgetsFactory::createLineEdit(p_parent);
+    m_nameLineEdit = WidgetsFactory::createLineEditWithSnippet(p_parent);
     auto validator = new QRegularExpressionValidator(QRegularExpression(PathUtils::c_fileNameRegularExpression),
     auto validator = new QRegularExpressionValidator(QRegularExpression(PathUtils::c_fileNameRegularExpression),
                                                      m_nameLineEdit);
                                                      m_nameLineEdit);
     m_nameLineEdit->setValidator(validator);
     m_nameLineEdit->setValidator(validator);
@@ -107,7 +108,7 @@ QLineEdit *NodeInfoWidget::getNameLineEdit() const
 
 
 QString NodeInfoWidget::getName() const
 QString NodeInfoWidget::getName() const
 {
 {
-    return getNameLineEdit()->text().trimmed();
+    return m_nameLineEdit->evaluatedText().trimmed();
 }
 }
 
 
 const Notebook *NodeInfoWidget::getNotebook() const
 const Notebook *NodeInfoWidget::getNotebook() const

+ 4 - 3
src/widgets/dialogs/nodeinfowidget.h

@@ -3,7 +3,7 @@
 
 
 #include <QWidget>
 #include <QWidget>
 
 
-#include "notebook/node.h"
+#include <notebook/node.h>
 
 
 class QLineEdit;
 class QLineEdit;
 class QLabel;
 class QLabel;
@@ -14,6 +14,7 @@ namespace vnotex
 {
 {
     class Notebook;
     class Notebook;
     class NodeLabelWithUpButton;
     class NodeLabelWithUpButton;
+    class LineEditWithSnippet;
 
 
     class NodeInfoWidget : public QWidget
     class NodeInfoWidget : public QWidget
     {
     {
@@ -50,13 +51,13 @@ namespace vnotex
 
 
         void setNode(const Node *p_node);
         void setNode(const Node *p_node);
 
 
-        Mode m_mode;
+        Mode m_mode = Mode::Create;
 
 
         QFormLayout *m_mainLayout = nullptr;
         QFormLayout *m_mainLayout = nullptr;
 
 
         QComboBox *m_fileTypeComboBox = nullptr;
         QComboBox *m_fileTypeComboBox = nullptr;
 
 
-        QLineEdit *m_nameLineEdit = nullptr;
+        LineEditWithSnippet *m_nameLineEdit = nullptr;
 
 
         NodeLabelWithUpButton *m_parentNodeLabel = nullptr;
         NodeLabelWithUpButton *m_parentNodeLabel = nullptr;
 
 

+ 3 - 2
src/widgets/dialogs/notebookinfowidget.cpp

@@ -18,6 +18,7 @@
 #include <utils/pathutils.h>
 #include <utils/pathutils.h>
 #include "exception.h"
 #include "exception.h"
 #include <utils/widgetutils.h>
 #include <utils/widgetutils.h>
+#include "../lineeditwithsnippet.h"
 
 
 using namespace vnotex;
 using namespace vnotex;
 
 
@@ -52,7 +53,7 @@ QGroupBox *NotebookInfoWidget::setupBasicInfoGroupBox(QWidget *p_parent)
     }
     }
 
 
     {
     {
-        m_nameLineEdit = WidgetsFactory::createLineEdit(box);
+        m_nameLineEdit = WidgetsFactory::createLineEditWithSnippet(box);
         m_nameLineEdit->setPlaceholderText(tr("Name of notebook"));
         m_nameLineEdit->setPlaceholderText(tr("Name of notebook"));
         connect(m_nameLineEdit, &QLineEdit::textEdited,
         connect(m_nameLineEdit, &QLineEdit::textEdited,
                 this, &NotebookInfoWidget::basicInfoEdited);
                 this, &NotebookInfoWidget::basicInfoEdited);
@@ -312,7 +313,7 @@ QComboBox *NotebookInfoWidget::getBackendComboBox() const
 
 
 QString NotebookInfoWidget::getName() const
 QString NotebookInfoWidget::getName() const
 {
 {
-    return getNameLineEdit()->text().trimmed();
+    return m_nameLineEdit->evaluatedText().trimmed();
 }
 }
 
 
 QString NotebookInfoWidget::getDescription() const
 QString NotebookInfoWidget::getDescription() const

+ 2 - 1
src/widgets/dialogs/notebookinfowidget.h

@@ -11,6 +11,7 @@ class QGroupBox;
 namespace vnotex
 namespace vnotex
 {
 {
     class Notebook;
     class Notebook;
+    class LineEditWithSnippet;
 
 
     class NotebookInfoWidget : public QWidget
     class NotebookInfoWidget : public QWidget
     {
     {
@@ -92,7 +93,7 @@ namespace vnotex
 
 
         const Notebook *m_notebook = nullptr;
         const Notebook *m_notebook = nullptr;
 
 
-        QLineEdit *m_nameLineEdit = nullptr;
+        LineEditWithSnippet *m_nameLineEdit = nullptr;
 
 
         QLineEdit *m_descriptionLineEdit = nullptr;
         QLineEdit *m_descriptionLineEdit = nullptr;
 
 

+ 6 - 11
src/widgets/dialogs/notepropertiesdialog.cpp

@@ -1,7 +1,5 @@
 #include "notepropertiesdialog.h"
 #include "notepropertiesdialog.h"
 
 
-#include <QtWidgets>
-
 #include "notebook/notebook.h"
 #include "notebook/notebook.h"
 #include "notebook/node.h"
 #include "notebook/node.h"
 #include "../widgetsfactory.h"
 #include "../widgetsfactory.h"
@@ -32,19 +30,16 @@ void NotePropertiesDialog::setupUI()
     setCentralWidget(m_infoWidget);
     setCentralWidget(m_infoWidget);
 
 
     setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
     setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
-    setButtonEnabled(QDialogButtonBox::Ok, false);
 
 
-    setWindowTitle(m_node->getName() + QStringLiteral(" ") + tr("Properties"));
+    setWindowTitle(tr("%1 Properties").arg(m_node->getName()));
 }
 }
 
 
 void NotePropertiesDialog::setupNodeInfoWidget(QWidget *p_parent)
 void NotePropertiesDialog::setupNodeInfoWidget(QWidget *p_parent)
 {
 {
     m_infoWidget = new NodeInfoWidget(m_node, p_parent);
     m_infoWidget = new NodeInfoWidget(m_node, p_parent);
-    connect(m_infoWidget, &NodeInfoWidget::inputEdited,
-            this, &NotePropertiesDialog::validateInputs);
 }
 }
 
 
-void NotePropertiesDialog::validateInputs()
+bool NotePropertiesDialog::validateInputs()
 {
 {
     bool valid = true;
     bool valid = true;
     QString msg;
     QString msg;
@@ -52,7 +47,7 @@ void NotePropertiesDialog::validateInputs()
     valid = valid && validateNameInput(msg);
     valid = valid && validateNameInput(msg);
     setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
     setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
                                   : ScrollDialog::InformationLevel::Error);
                                   : ScrollDialog::InformationLevel::Error);
-    setButtonEnabled(QDialogButtonBox::Ok, valid);
+    return valid;
 }
 }
 
 
 bool NotePropertiesDialog::validateNameInput(QString &p_msg)
 bool NotePropertiesDialog::validateNameInput(QString &p_msg)
@@ -60,8 +55,8 @@ bool NotePropertiesDialog::validateNameInput(QString &p_msg)
     p_msg.clear();
     p_msg.clear();
 
 
     auto name = m_infoWidget->getName();
     auto name = m_infoWidget->getName();
-    if (name.isEmpty()) {
-        p_msg = tr("Please specify a name for the note.");
+    if (name.isEmpty() || !PathUtils::isLegalFileName(name)) {
+        p_msg = tr("Please specify a valid name for the note.");
         return false;
         return false;
     }
     }
 
 
@@ -76,7 +71,7 @@ bool NotePropertiesDialog::validateNameInput(QString &p_msg)
 
 
 void NotePropertiesDialog::acceptedButtonClicked()
 void NotePropertiesDialog::acceptedButtonClicked()
 {
 {
-    if (saveNoteProperties()) {
+    if (validateInputs() && saveNoteProperties()) {
         accept();
         accept();
     }
     }
 }
 }

+ 2 - 3
src/widgets/dialogs/notepropertiesdialog.h

@@ -17,9 +17,6 @@ namespace vnotex
     protected:
     protected:
         void acceptedButtonClicked() Q_DECL_OVERRIDE;
         void acceptedButtonClicked() Q_DECL_OVERRIDE;
 
 
-    private slots:
-        void validateInputs();
-
     private:
     private:
         void setupUI();
         void setupUI();
 
 
@@ -29,6 +26,8 @@ namespace vnotex
 
 
         bool saveNoteProperties();
         bool saveNoteProperties();
 
 
+        bool validateInputs();
+
         NodeInfoWidget *m_infoWidget = nullptr;
         NodeInfoWidget *m_infoWidget = nullptr;
 
 
         Node *m_node = nullptr;
         Node *m_node = nullptr;

+ 10 - 0
src/widgets/dialogs/settings/quickaccesspage.cpp

@@ -5,6 +5,7 @@
 #include <QGroupBox>
 #include <QGroupBox>
 #include <QPlainTextEdit>
 #include <QPlainTextEdit>
 #include <QDebug>
 #include <QDebug>
+#include <QFileDialog>
 
 
 #include <widgets/widgetsfactory.h>
 #include <widgets/widgetsfactory.h>
 #include <core/sessionconfig.h>
 #include <core/sessionconfig.h>
@@ -79,6 +80,15 @@ QGroupBox *QuickAccessPage::setupFlashPageGroup()
         addSearchItem(label, m_flashPageInput->toolTip(), m_flashPageInput);
         addSearchItem(label, m_flashPageInput->toolTip(), m_flashPageInput);
         connect(m_flashPageInput, &LocationInputWithBrowseButton::textChanged,
         connect(m_flashPageInput, &LocationInputWithBrowseButton::textChanged,
                 this, &QuickAccessPage::pageIsChanged);
                 this, &QuickAccessPage::pageIsChanged);
+        connect(m_flashPageInput, &LocationInputWithBrowseButton::clicked,
+                this, [this]() {
+                    auto filePath = QFileDialog::getOpenFileName(this,
+                                                                 tr("Select Flash Page File"),
+                                                                 QDir::homePath());
+                    if (!filePath.isEmpty()) {
+                        m_flashPageInput->setText(filePath);
+                    }
+                });
     }
     }
 
 
     return box;
     return box;

+ 173 - 0
src/widgets/dialogs/snippetinfowidget.cpp

@@ -0,0 +1,173 @@
+#include "snippetinfowidget.h"
+
+#include <QLineEdit>
+#include <QPlainTextEdit>
+#include <QComboBox>
+#include <QFormLayout>
+#include <QCheckBox>
+
+#include <widgets/widgetsfactory.h>
+#include <snippet/snippet.h>
+#include <snippet/snippetmgr.h>
+#include <utils/utils.h>
+#include <utils/pathutils.h>
+
+using namespace vnotex;
+
+SnippetInfoWidget::SnippetInfoWidget(QWidget *p_parent)
+    : QWidget(p_parent),
+      m_mode(Mode::Create)
+{
+    setupUI();
+}
+
+SnippetInfoWidget::SnippetInfoWidget(const Snippet *p_snippet, QWidget *p_parent)
+    : QWidget(p_parent),
+      m_mode(Mode::Edit)
+{
+    setupUI();
+
+    setSnippet(p_snippet);
+}
+
+void SnippetInfoWidget::setupUI()
+{
+    auto mainLayout = new QFormLayout(this);
+
+    m_nameLineEdit = WidgetsFactory::createLineEdit(this);
+    auto validator = new QRegularExpressionValidator(QRegularExpression(PathUtils::c_fileNameRegularExpression),
+                                                     m_nameLineEdit);
+    m_nameLineEdit->setValidator(validator);
+    connect(m_nameLineEdit, &QLineEdit::textEdited,
+            this, &SnippetInfoWidget::inputEdited);
+    mainLayout->addRow(tr("Name:"), m_nameLineEdit);
+
+    setFocusProxy(m_nameLineEdit);
+
+    setupTypeComboBox(this);
+    mainLayout->addRow(tr("Type:"), m_typeComboBox);
+
+    setupShortcutComboBox(this);
+    mainLayout->addRow(tr("Shortcut:"), m_shortcutComboBox);
+
+    m_cursorMarkLineEdit = WidgetsFactory::createLineEdit(this);
+    m_cursorMarkLineEdit->setText(Snippet::c_defaultCursorMark);
+    m_cursorMarkLineEdit->setToolTip(tr("A mark in the snippet content indicating the cursor position after the application"));
+    connect(m_cursorMarkLineEdit, &QLineEdit::textEdited,
+            this, &SnippetInfoWidget::inputEdited);
+    mainLayout->addRow(tr("Cursor mark:"), m_cursorMarkLineEdit);
+
+    m_selectionMarkLineEdit = WidgetsFactory::createLineEdit(this);
+    m_selectionMarkLineEdit->setText(Snippet::c_defaultSelectionMark);
+    m_selectionMarkLineEdit->setToolTip(tr("A mark in the snippet content that will be replaced with the selected text before the application"));
+    connect(m_selectionMarkLineEdit, &QLineEdit::textEdited,
+            this, &SnippetInfoWidget::inputEdited);
+    mainLayout->addRow(tr("Selection mark:"), m_selectionMarkLineEdit);
+
+    m_indentAsFirstLineCheckBox = WidgetsFactory::createCheckBox(tr("Indent as first line"), this);
+    m_indentAsFirstLineCheckBox->setChecked(true);
+    connect(m_indentAsFirstLineCheckBox, &QCheckBox::stateChanged,
+            this, &SnippetInfoWidget::inputEdited);
+    mainLayout->addRow(m_indentAsFirstLineCheckBox);
+
+    m_contentTextEdit = WidgetsFactory::createPlainTextEdit(this);
+    connect(m_contentTextEdit, &QPlainTextEdit::textChanged,
+            this, &SnippetInfoWidget::inputEdited);
+    mainLayout->addRow(tr("Content:"), m_contentTextEdit);
+}
+
+void SnippetInfoWidget::setupTypeComboBox(QWidget *p_parent)
+{
+    m_typeComboBox = WidgetsFactory::createComboBox(p_parent);
+    m_typeComboBox->addItem(tr("Text"), static_cast<int>(Snippet::Type::Text));
+    if (m_mode == Mode::Edit) {
+        m_typeComboBox->addItem(tr("Dynamic"), static_cast<int>(Snippet::Type::Dynamic));
+    }
+    connect(m_typeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
+            this, [this]() {
+                emit inputEdited();
+            });
+}
+
+void SnippetInfoWidget::setupShortcutComboBox(QWidget *p_parent)
+{
+    m_shortcutComboBox = WidgetsFactory::createComboBox(p_parent);
+    if (m_mode == Mode::Create) {
+        initShortcutComboBox();
+    }
+    connect(m_shortcutComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
+            this, &SnippetInfoWidget::inputEdited);
+}
+
+QString SnippetInfoWidget::getName() const
+{
+    return m_nameLineEdit->text();
+}
+
+Snippet::Type SnippetInfoWidget::getType() const
+{
+    return static_cast<Snippet::Type>(m_typeComboBox->currentData().toInt());
+}
+
+int SnippetInfoWidget::getShortcut() const
+{
+    return m_shortcutComboBox->currentData().toInt();
+}
+
+QString SnippetInfoWidget::getCursorMark() const
+{
+    return m_cursorMarkLineEdit->text();
+}
+
+QString SnippetInfoWidget::getSelectionMark() const
+{
+    return m_selectionMarkLineEdit->text();
+}
+
+bool SnippetInfoWidget::shouldIndentAsFirstLine() const
+{
+    return m_indentAsFirstLineCheckBox->isChecked();
+}
+
+QString SnippetInfoWidget::getContent() const
+{
+    return m_contentTextEdit->toPlainText();
+}
+
+void SnippetInfoWidget::setSnippet(const Snippet *p_snippet)
+{
+    if (m_snippet == p_snippet) {
+        return;
+    }
+
+    Q_ASSERT(m_mode == Mode::Edit);
+    m_snippet = p_snippet;
+    initShortcutComboBox();
+    if (m_snippet) {
+        m_nameLineEdit->setText(m_snippet->getName());
+        m_typeComboBox->setCurrentIndex(m_typeComboBox->findData(static_cast<int>(m_snippet->getType())));
+        m_shortcutComboBox->setCurrentIndex(m_shortcutComboBox->findData(m_snippet->getShortcut()));
+        m_cursorMarkLineEdit->setText(m_snippet->getCursorMark());
+        m_selectionMarkLineEdit->setText(m_snippet->getSelectionMark());
+        m_indentAsFirstLineCheckBox->setChecked(m_snippet->isIndentAsFirstLineEnabled());
+        m_contentTextEdit->setPlainText(m_snippet->getContent());
+    } else {
+        m_nameLineEdit->clear();
+        m_typeComboBox->setCurrentIndex(m_typeComboBox->findData(static_cast<int>(Snippet::Type::Text)));
+        m_shortcutComboBox->setCurrentIndex(m_shortcutComboBox->findData(Snippet::InvalidShortcut));
+        m_cursorMarkLineEdit->setText(Snippet::c_defaultCursorMark);
+        m_selectionMarkLineEdit->setText(Snippet::c_defaultSelectionMark);
+        m_indentAsFirstLineCheckBox->setChecked(true);
+        m_contentTextEdit->clear();
+    }
+}
+
+void SnippetInfoWidget::initShortcutComboBox()
+{
+    m_shortcutComboBox->clear();
+    m_shortcutComboBox->addItem(tr("None"), Snippet::InvalidShortcut);
+    const auto shortcuts = SnippetMgr::getInst().getAvailableShortcuts(m_snippet ? m_snippet->getShortcut() : Snippet::InvalidShortcut);
+    for (auto sh : shortcuts) {
+        m_shortcutComboBox->addItem(Utils::intToString(sh, 2), sh);
+    }
+}

+ 73 - 0
src/widgets/dialogs/snippetinfowidget.h

@@ -0,0 +1,73 @@
+#ifndef SNIPPETINFOWIDGET_H
+#define SNIPPETINFOWIDGET_H
+
+#include <QWidget>
+
+#include <snippet/snippet.h>
+
+class QLineEdit;
+class QComboBox;
+class QCheckBox;
+class QPlainTextEdit;
+
+namespace vnotex
+{
+    class SnippetInfoWidget : public QWidget
+    {
+        Q_OBJECT
+    public:
+        enum Mode { Create, Edit };
+
+        explicit SnippetInfoWidget(QWidget *p_parent = nullptr);
+
+        SnippetInfoWidget(const Snippet *p_snippet, QWidget *p_parent = nullptr);
+
+        QString getName() const;
+
+        Snippet::Type getType() const;
+
+        int getShortcut() const;
+
+        QString getCursorMark() const;
+
+        QString getSelectionMark() const;
+
+        bool shouldIndentAsFirstLine() const;
+
+        QString getContent() const;
+
+    signals:
+        void inputEdited();
+
+    private:
+        void setupUI();
+
+        void setupTypeComboBox(QWidget *p_parent);
+
+        void setupShortcutComboBox(QWidget *p_parent);
+
+        void setSnippet(const Snippet *p_snippet);
+
+        void initShortcutComboBox();
+
+        Mode m_mode = Mode::Create;
+
+        const Snippet *m_snippet = nullptr;
+
+        QLineEdit *m_nameLineEdit = nullptr;
+
+        QComboBox *m_typeComboBox = nullptr;
+
+        QComboBox *m_shortcutComboBox = nullptr;
+
+        QLineEdit *m_cursorMarkLineEdit = nullptr;
+
+        QLineEdit *m_selectionMarkLineEdit = nullptr;
+
+        QCheckBox *m_indentAsFirstLineCheckBox = nullptr;
+
+        QPlainTextEdit *m_contentTextEdit = nullptr;
+    };
+}
+
+#endif // SNIPPETINFOWIDGET_H

+ 98 - 0
src/widgets/dialogs/snippetpropertiesdialog.cpp

@@ -0,0 +1,98 @@
+#include "snippetpropertiesdialog.h"
+
+#include <snippet/snippet.h>
+#include <snippet/snippetmgr.h>
+#include <core/exception.h>
+
+#include "snippetinfowidget.h"
+
+using namespace vnotex;
+
+SnippetPropertiesDialog::SnippetPropertiesDialog(Snippet *p_snippet, QWidget *p_parent)
+    : ScrollDialog(p_parent),
+      m_snippet(p_snippet)
+{
+    Q_ASSERT(m_snippet);
+    setupUI();
+
+    m_infoWidget->setFocus();
+}
+
+void SnippetPropertiesDialog::setupUI()
+{
+    setupSnippetInfoWidget(this);
+    setCentralWidget(m_infoWidget);
+
+    setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    if (m_snippet->isReadOnly()) {
+        setButtonEnabled(QDialogButtonBox::Ok, false);
+    }
+
+    setWindowTitle(tr("%1 Properties").arg(m_snippet->getName()));
+}
+
+void SnippetPropertiesDialog::setupSnippetInfoWidget(QWidget *p_parent)
+{
+    m_infoWidget = new SnippetInfoWidget(m_snippet, p_parent);
+}
+
+bool SnippetPropertiesDialog::validateInputs()
+{
+    bool valid = true;
+    QString msg;
+
+    valid = valid && validateNameInput(msg);
+    setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info
+                                  : ScrollDialog::InformationLevel::Error);
+    return valid;
+}
+
+bool SnippetPropertiesDialog::validateNameInput(QString &p_msg)
+{
+    p_msg.clear();
+
+    auto name = m_infoWidget->getName();
+    if (name.isEmpty()) {
+        p_msg = tr("Please specify a name for the snippet.");
+        return false;
+    }
+
+    if (name.toLower() == m_snippet->getName().toLower()) {
+        return true;
+    }
+
+    if (SnippetMgr::getInst().find(name)) {
+        p_msg = tr("Name conflicts with existing snippet.");
+        return false;
+    }
+
+    return true;
+}
+
+void SnippetPropertiesDialog::acceptedButtonClicked()
+{
+    if (validateInputs() && saveSnippetProperties()) {
+        accept();
+    }
+}
+
+bool SnippetPropertiesDialog::saveSnippetProperties()
+{
+    auto snip = QSharedPointer<Snippet>::create(m_infoWidget->getName(),
+                                                m_infoWidget->getContent(),
+                                                m_infoWidget->getShortcut(),
+                                                m_infoWidget->shouldIndentAsFirstLine(),
+                                                m_infoWidget->getCursorMark(),
+                                                m_infoWidget->getSelectionMark());
+    Q_ASSERT(snip->isValid());
+    try {
+        SnippetMgr::getInst().updateSnippet(m_snippet->getName(), snip);
+    } catch (Exception &p_e) {
+        QString msg = tr("Failed to update snippet (%1) (%2).")
+                        .arg(snip->getName(), p_e.what());
+        qWarning() << msg;
+        setInformationText(msg, ScrollDialog::InformationLevel::Error);
+        return false;
+    }
+    return true;
+}

+ 37 - 0
src/widgets/dialogs/snippetpropertiesdialog.h

@@ -0,0 +1,37 @@
+#ifndef SNIPPETPROPERTIESDIALOG_H
+#define SNIPPETPROPERTIESDIALOG_H
+
+#include "scrolldialog.h"
+
+namespace vnotex
+{
+    class Snippet;
+    class SnippetInfoWidget;
+
+    class SnippetPropertiesDialog : public ScrollDialog
+    {
+        Q_OBJECT
+    public:
+        SnippetPropertiesDialog(Snippet *p_snippet, QWidget *p_parent = nullptr);
+
+    protected:
+        void acceptedButtonClicked() Q_DECL_OVERRIDE;
+
+    private:
+        void setupUI();
+
+        void setupSnippetInfoWidget(QWidget *p_parent);
+
+        bool validateNameInput(QString &p_msg);
+
+        bool saveSnippetProperties();
+
+        bool validateInputs();
+
+        SnippetInfoWidget *m_infoWidget = nullptr;
+
+        Snippet *m_snippet = nullptr;
+    };
+}
+
+#endif // SNIPPETPROPERTIESDIALOG_H

+ 29 - 0
src/widgets/lineeditwithsnippet.cpp

@@ -0,0 +1,29 @@
+#include "lineeditwithsnippet.h"
+
+#include <snippet/snippetmgr.h>
+
+using namespace vnotex;
+
+LineEditWithSnippet::LineEditWithSnippet(QWidget *p_parent)
+    : LineEdit(p_parent)
+{
+    setTips();
+}
+
+LineEditWithSnippet::LineEditWithSnippet(const QString &p_contents, QWidget *p_parent)
+    : LineEdit(p_contents, p_parent)
+{
+    setTips();
+}
+
+void LineEditWithSnippet::setTips()
+{
+    const auto tips = tr("Snippet is supported via %name%");
+    setToolTip(tips);
+    setPlaceholderText(tips);
+}
+
+QString LineEditWithSnippet::evaluatedText() const
+{
+    return SnippetMgr::getInst().applySnippetBySymbol(text());
+}

+ 25 - 0
src/widgets/lineeditwithsnippet.h

@@ -0,0 +1,25 @@
+#ifndef LINEEDITWITHSNIPPET_H
+#define LINEEDITWITHSNIPPET_H
+
+#include "lineedit.h"
+
+namespace vnotex
+{
+    // A line edit with snippet support.
+    class LineEditWithSnippet : public LineEdit
+    {
+        Q_OBJECT
+    public:
+        explicit LineEditWithSnippet(QWidget *p_parent = nullptr);
+
+        LineEditWithSnippet(const QString &p_contents, QWidget *p_parent = nullptr);
+
+        // Get text with snippets evaluated.
+        QString evaluatedText() const;
+
+    private:
+        void setTips();
+    };
+}
+
+#endif // LINEEDITWITHSNIPPET_H

+ 4 - 3
src/widgets/locationlist.cpp

@@ -9,6 +9,7 @@
 
 
 #include <core/vnotex.h>
 #include <core/vnotex.h>
 #include <utils/iconutils.h>
 #include <utils/iconutils.h>
+#include <utils/widgetutils.h>
 
 
 using namespace vnotex;
 using namespace vnotex;
 
 
@@ -29,8 +30,7 @@ LocationList::LocationList(QWidget *p_parent)
 void LocationList::setupUI()
 void LocationList::setupUI()
 {
 {
     auto mainLayout = new QVBoxLayout(this);
     auto mainLayout = new QVBoxLayout(this);
-    mainLayout->setContentsMargins(0, 0, 0, 0);
-    mainLayout->setSpacing(0);
+    WidgetUtils::setContentsMargins(mainLayout);
 
 
     {
     {
         setupTitleBar(QString(), this);
         setupTitleBar(QString(), this);
@@ -96,6 +96,7 @@ NavigationModeWrapper<QTreeWidget, QTreeWidgetItem> *LocationList::getNavigation
 void LocationList::setupTitleBar(const QString &p_title, QWidget *p_parent)
 void LocationList::setupTitleBar(const QString &p_title, QWidget *p_parent)
 {
 {
     m_titleBar = new TitleBar(p_title, true, TitleBar::Action::None, p_parent);
     m_titleBar = new TitleBar(p_title, true, TitleBar::Action::None, p_parent);
+    m_titleBar->setActionButtonsAlwaysShown(true);
 
 
     {
     {
         auto clearBtn = m_titleBar->addActionButton(QStringLiteral("clear.svg"), tr("Clear"));
         auto clearBtn = m_titleBar->addActionButton(QStringLiteral("clear.svg"), tr("Clear"));
@@ -175,6 +176,6 @@ void LocationList::updateItemsCountLabel()
     if (cnt == 0) {
     if (cnt == 0) {
         m_titleBar->setInfoLabel("");
         m_titleBar->setInfoLabel("");
     } else {
     } else {
-        m_titleBar->setInfoLabel(tr("%n Item(s)", "", m_tree->topLevelItemCount()));
+        m_titleBar->setInfoLabel(tr("%n Item(s)", "", cnt));
     }
     }
 }
 }

+ 55 - 15
src/widgets/mainwindow.cpp

@@ -41,6 +41,7 @@
 #include "titletoolbar.h"
 #include "titletoolbar.h"
 #include "locationlist.h"
 #include "locationlist.h"
 #include "searchpanel.h"
 #include "searchpanel.h"
+#include "snippetpanel.h"
 #include <notebook/notebook.h>
 #include <notebook/notebook.h>
 #include "searchinfoprovider.h"
 #include "searchinfoprovider.h"
 #include <vtextedit/spellchecker.h>
 #include <vtextedit/spellchecker.h>
@@ -84,14 +85,15 @@ void MainWindow::kickOffOnStart(const QStringList &p_paths)
 
 
         VNoteX::getInst().initLoad();
         VNoteX::getInst().initLoad();
 
 
+        setupSpellCheck();
+
+        // Do necessary stuffs before emitting this signal.
         emit mainWindowStarted();
         emit mainWindowStarted();
 
 
         emit layoutChanged();
         emit layoutChanged();
 
 
         demoWidget();
         demoWidget();
 
 
-        setupSpellCheck();
-
         openFiles(p_paths);
         openFiles(p_paths);
     });
     });
 }
 }
@@ -206,6 +208,8 @@ void MainWindow::setupDocks()
 
 
     setupSearchDock();
     setupSearchDock();
 
 
+    setupSnippetDock();
+
     for (int i = 1; i < m_docks.size(); ++i) {
     for (int i = 1; i < m_docks.size(); ++i) {
         tabifyDockWidget(m_docks[i - 1], m_docks[i]);
         tabifyDockWidget(m_docks[i - 1], m_docks[i]);
     }
     }
@@ -295,6 +299,34 @@ void MainWindow::setupSearchPanel()
     m_searchPanel->setObjectName("SearchPanel.vnotex");
     m_searchPanel->setObjectName("SearchPanel.vnotex");
 }
 }
 
 
+void MainWindow::setupSnippetDock()
+{
+    auto dock = new QDockWidget(tr("Snippets"), this);
+    m_docks.push_back(dock);
+
+    dock->setObjectName(QStringLiteral("SnippetDock.vnotex"));
+    dock->setAllowedAreas(Qt::AllDockWidgetAreas);
+
+    setupSnippetPanel();
+    dock->setWidget(m_snippetPanel);
+    dock->setFocusProxy(m_snippetPanel);
+    addDockWidget(Qt::LeftDockWidgetArea, dock);
+}
+
+void MainWindow::setupSnippetPanel()
+{
+    m_snippetPanel = new SnippetPanel(this);
+    m_snippetPanel->setObjectName("SnippetPanel.vnotex");
+    connect(m_snippetPanel, &SnippetPanel::applySnippetRequested,
+            this, [this](const QString &p_name) {
+                auto viewWindow = m_viewArea->getCurrentViewWindow();
+                if (viewWindow) {
+                    viewWindow->applySnippet(p_name);
+                    viewWindow->setFocus();
+                }
+            });
+}
+
 void MainWindow::setupLocationListDock()
 void MainWindow::setupLocationListDock()
 {
 {
     auto dock = new QDockWidget(tr("Location List"), this);
     auto dock = new QDockWidget(tr("Location List"), this);
@@ -418,7 +450,7 @@ void MainWindow::saveStateAndGeometry()
     SessionConfig::MainWindowStateGeometry sg;
     SessionConfig::MainWindowStateGeometry sg;
     sg.m_mainState = saveState();
     sg.m_mainState = saveState();
     sg.m_mainGeometry = saveGeometry();
     sg.m_mainGeometry = saveGeometry();
-    sg.m_docksVisibilityBeforeExpand = m_docksVisibilityBeforeExpand;
+    sg.m_visibleDocksBeforeExpand = m_visibleDocksBeforeExpand;
 
 
     auto& sessionConfig = ConfigMgr::getInst().getSessionConfig();
     auto& sessionConfig = ConfigMgr::getInst().getSessionConfig();
     sessionConfig.setMainWindowStateGeometry(sg);
     sessionConfig.setMainWindowStateGeometry(sg);
@@ -439,12 +471,13 @@ void MainWindow::loadStateAndGeometry(bool p_stateOnly)
     }
     }
 
 
     if (!p_stateOnly) {
     if (!p_stateOnly) {
-        m_docksVisibilityBeforeExpand = sg.m_docksVisibilityBeforeExpand;
-        if (m_docksVisibilityBeforeExpand.isEmpty()) {
-            // Init.
-            m_docksVisibilityBeforeExpand.resize(m_docks.size());
+        m_visibleDocksBeforeExpand = sg.m_visibleDocksBeforeExpand;
+        if (m_visibleDocksBeforeExpand.isEmpty()) {
+            // Init (or init again if there is no visible dock).
             for (int i = 0; i < m_docks.size(); ++i) {
             for (int i = 0; i < m_docks.size(); ++i) {
-                m_docksVisibilityBeforeExpand.setBit(i, m_docks[i]->isVisible());
+                if (m_docks[i]->isVisible()) {
+                    m_visibleDocksBeforeExpand.push_back(m_docks[i]->objectName());
+                }
             }
             }
         }
         }
     }
     }
@@ -468,10 +501,14 @@ void MainWindow::setContentAreaExpanded(bool p_expanded)
 
 
     if (p_expanded) {
     if (p_expanded) {
         // Store the state and hide.
         // Store the state and hide.
+        m_visibleDocksBeforeExpand.clear();
         for (int i = 0; i < m_docks.size(); ++i) {
         for (int i = 0; i < m_docks.size(); ++i) {
-            m_docksVisibilityBeforeExpand[i] = m_docks[i]->isVisible();
+            const auto objName = m_docks[i]->objectName();
+            if (m_docks[i]->isVisible()) {
+                m_visibleDocksBeforeExpand.push_back(objName);
+            }
 
 
-            if (m_docks[i]->isFloating() || keepDocks.contains(m_docks[i]->objectName())) {
+            if (m_docks[i]->isFloating() || keepDocks.contains(objName)) {
                 continue;
                 continue;
             }
             }
 
 
@@ -481,15 +518,15 @@ void MainWindow::setContentAreaExpanded(bool p_expanded)
         // Restore the state.
         // Restore the state.
         bool hasVisible = false;
         bool hasVisible = false;
         for (int i = 0; i < m_docks.size(); ++i) {
         for (int i = 0; i < m_docks.size(); ++i) {
-            if (m_docks[i]->isFloating() || keepDocks.contains(m_docks[i]->objectName())) {
+            const auto objName = m_docks[i]->objectName();
+            if (m_docks[i]->isFloating() || keepDocks.contains(objName)) {
                 continue;
                 continue;
             }
             }
 
 
-            if (m_docksVisibilityBeforeExpand[i]) {
-                hasVisible = true;
-            }
+            const bool visible = m_visibleDocksBeforeExpand.contains(objName);
+            hasVisible = hasVisible || visible;
 
 
-            m_docks[i]->setVisible(m_docksVisibilityBeforeExpand[i]);
+            m_docks[i]->setVisible(visible);
         }
         }
 
 
         if (!hasVisible) {
         if (!hasVisible) {
@@ -606,6 +643,9 @@ void MainWindow::setupShortcuts()
 
 
     setupDockActivateShortcut(m_docks[DockIndex::LocationListDock],
     setupDockActivateShortcut(m_docks[DockIndex::LocationListDock],
                               coreConfig.getShortcut(CoreConfig::Shortcut::LocationListDock));
                               coreConfig.getShortcut(CoreConfig::Shortcut::LocationListDock));
+
+    setupDockActivateShortcut(m_docks[DockIndex::SnippetDock],
+                              coreConfig.getShortcut(CoreConfig::Shortcut::SnippetDock));
 }
 }
 
 
 void MainWindow::setupDockActivateShortcut(QDockWidget *p_dock, const QString &p_keys)
 void MainWindow::setupDockActivateShortcut(QDockWidget *p_dock, const QString &p_keys)

+ 9 - 1
src/widgets/mainwindow.h

@@ -22,6 +22,7 @@ namespace vnotex
     class OutlineViewer;
     class OutlineViewer;
     class LocationList;
     class LocationList;
     class SearchPanel;
     class SearchPanel;
+    class SnippetPanel;
 
 
     enum { RESTART_EXIT_CODE = 1000 };
     enum { RESTART_EXIT_CODE = 1000 };
 
 
@@ -96,6 +97,7 @@ namespace vnotex
             NavigationDock = 0,
             NavigationDock = 0,
             OutlineDock,
             OutlineDock,
             SearchDock,
             SearchDock,
+            SnippetDock,
             LocationListDock
             LocationListDock
         };
         };
 
 
@@ -117,6 +119,10 @@ namespace vnotex
 
 
         void setupLocationList();
         void setupLocationList();
 
 
+        void setupSnippetDock();
+
+        void setupSnippetPanel();
+
         void setupNotebookExplorer(QWidget *p_parent = nullptr);
         void setupNotebookExplorer(QWidget *p_parent = nullptr);
 
 
         void setupDocks();
         void setupDocks();
@@ -168,6 +174,8 @@ namespace vnotex
 
 
         SearchPanel *m_searchPanel = nullptr;
         SearchPanel *m_searchPanel = nullptr;
 
 
+        SnippetPanel *m_snippetPanel = nullptr;
+
         QVector<QDockWidget *> m_docks;
         QVector<QDockWidget *> m_docks;
 
 
         bool m_layoutReset = false;
         bool m_layoutReset = false;
@@ -184,7 +192,7 @@ namespace vnotex
 
 
         QTimer *m_tipsTimer = nullptr;
         QTimer *m_tipsTimer = nullptr;
 
 
-        QBitArray m_docksVisibilityBeforeExpand;
+        QStringList m_visibleDocksBeforeExpand;
     };
     };
 } // ns vnotex
 } // ns vnotex
 
 

+ 14 - 0
src/widgets/markdownviewwindow.cpp

@@ -31,6 +31,7 @@
 #include "editors/statuswidget.h"
 #include "editors/statuswidget.h"
 #include "editors/plantumlhelper.h"
 #include "editors/plantumlhelper.h"
 #include "editors/graphvizhelper.h"
 #include "editors/graphvizhelper.h"
+#include <snippet/snippetmgr.h>
 
 
 using namespace vnotex;
 using namespace vnotex;
 
 
@@ -975,3 +976,16 @@ void MarkdownViewWindow::setupPreviewHelper()
                                    markdownEditorConfig.getPlantUmlCommand());
                                    markdownEditorConfig.getPlantUmlCommand());
     GraphvizHelper::getInst().init(markdownEditorConfig.getGraphvizExe());
     GraphvizHelper::getInst().init(markdownEditorConfig.getGraphvizExe());
 }
 }
+
+void MarkdownViewWindow::applySnippet(const QString &p_name)
+{
+    if (isReadMode() || m_editor->isReadOnly()) {
+        qWarning() << "failed to apply snippet in read mode or to a read-only buffer" << p_name;
+        return;
+    }
+
+    m_editor->enterInsertModeIfApplicable();
+    SnippetMgr::getInst().applySnippet(p_name,
+                                       m_editor->getTextEdit(),
+                                       SnippetMgr::generateOverrides(getBuffer()));
+}

+ 2 - 0
src/widgets/markdownviewwindow.h

@@ -44,6 +44,8 @@ namespace vnotex
 
 
         ViewWindowSession saveSession() const Q_DECL_OVERRIDE;
         ViewWindowSession saveSession() const Q_DECL_OVERRIDE;
 
 
+        void applySnippet(const QString &p_name) Q_DECL_OVERRIDE;
+
     public slots:
     public slots:
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
 
 

+ 1 - 0
src/widgets/notebookexplorer.cpp

@@ -105,6 +105,7 @@ TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent)
                                  TitleBar::Action::Menu,
                                  TitleBar::Action::Menu,
                                  p_parent);
                                  p_parent);
     titleBar->setWhatsThis(tr("This title bar contains buttons and menu to manage notebooks and notes."));
     titleBar->setWhatsThis(tr("This title bar contains buttons and menu to manage notebooks and notes."));
+    titleBar->setActionButtonsAlwaysShown(true);
 
 
     {
     {
         auto viewMenu = WidgetsFactory::createMenu(titleBar);
         auto viewMenu = WidgetsFactory::createMenu(titleBar);

+ 1 - 0
src/widgets/outlineviewer.cpp

@@ -90,6 +90,7 @@ NavigationModeWrapper<QTreeWidget, QTreeWidgetItem> *OutlineViewer::getNavigatio
 TitleBar *OutlineViewer::setupTitleBar(const QString &p_title, QWidget *p_parent)
 TitleBar *OutlineViewer::setupTitleBar(const QString &p_title, QWidget *p_parent)
 {
 {
     auto titleBar = new TitleBar(p_title, false, TitleBar::Action::None, p_parent);
     auto titleBar = new TitleBar(p_title, false, TitleBar::Action::None, p_parent);
+    titleBar->setActionButtonsAlwaysShown(true);
 
 
     auto decreaseBtn = titleBar->addActionButton(QStringLiteral("decrease_outline_level.svg"), tr("Decrease Expansion Level"));
     auto decreaseBtn = titleBar->addActionButton(QStringLiteral("decrease_outline_level.svg"), tr("Decrease Expansion Level"));
     connect(decreaseBtn, &QToolButton::clicked,
     connect(decreaseBtn, &QToolButton::clicked,

+ 1 - 1
src/widgets/searchpanel.h

@@ -47,7 +47,7 @@ namespace vnotex
     {
     {
         Q_OBJECT
         Q_OBJECT
     public:
     public:
-        explicit SearchPanel(const QSharedPointer<ISearchInfoProvider> &p_provider, QWidget *p_parent = nullptr);
+        SearchPanel(const QSharedPointer<ISearchInfoProvider> &p_provider, QWidget *p_parent = nullptr);
 
 
     private slots:
     private slots:
         void startSearch();
         void startSearch();

+ 232 - 0
src/widgets/snippetpanel.cpp

@@ -0,0 +1,232 @@
+#include "snippetpanel.h"
+
+#include <QVBoxLayout>
+#include <QToolButton>
+#include <QListWidgetItem>
+
+#include <utils/widgetutils.h>
+#include <snippet/snippetmgr.h>
+#include <core/vnotex.h>
+#include <core/exception.h>
+
+#include "titlebar.h"
+#include "listwidget.h"
+#include "dialogs/newsnippetdialog.h"
+#include "dialogs/snippetpropertiesdialog.h"
+#include "dialogs/deleteconfirmdialog.h"
+#include "mainwindow.h"
+#include "messageboxhelper.h"
+
+using namespace vnotex;
+
+SnippetPanel::SnippetPanel(QWidget *p_parent)
+    : QFrame(p_parent)
+{
+    setupUI();
+}
+
+void SnippetPanel::setupUI()
+{
+    auto mainLayout = new QVBoxLayout(this);
+    WidgetUtils::setContentsMargins(mainLayout);
+
+    {
+        setupTitleBar(QString(), this);
+        mainLayout->addWidget(m_titleBar);
+    }
+
+    m_snippetList = new ListWidget(this);
+    m_snippetList->setContextMenuPolicy(Qt::CustomContextMenu);
+    m_snippetList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+    connect(m_snippetList, &QListWidget::customContextMenuRequested,
+            this, &SnippetPanel::handleContextMenuRequested);
+    connect(m_snippetList, &QListWidget::itemActivated,
+            this, &SnippetPanel::applySnippet);
+    mainLayout->addWidget(m_snippetList);
+
+    setFocusProxy(m_snippetList);
+}
+
+void SnippetPanel::setupTitleBar(const QString &p_title, QWidget *p_parent)
+{
+    m_titleBar = new TitleBar(p_title, true, TitleBar::Action::None, p_parent);
+    m_titleBar->setActionButtonsAlwaysShown(true);
+
+    {
+        auto newBtn = m_titleBar->addActionButton(QStringLiteral("add.svg"), tr("New Snippet"));
+        connect(newBtn, &QToolButton::triggered,
+                this, &SnippetPanel::newSnippet);
+    }
+
+    {
+        auto openFolderBtn = m_titleBar->addActionButton(QStringLiteral("open_folder.svg"), tr("Open Folder"));
+        connect(openFolderBtn, &QToolButton::triggered,
+                this, [this]() {
+                    WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(SnippetMgr::getInst().getSnippetFolder()));
+                });
+    }
+}
+
+void SnippetPanel::newSnippet()
+{
+    NewSnippetDialog dialog(VNoteX::getInst().getMainWindow());
+    if (dialog.exec() == QDialog::Accepted) {
+        updateSnippetList();
+    }
+}
+
+void SnippetPanel::updateItemsCountLabel()
+{
+    const auto cnt = m_snippetList->count();
+    if (cnt == 0) {
+        m_titleBar->setInfoLabel("");
+    } else {
+        m_titleBar->setInfoLabel(tr("%n Item(s)", "", cnt));
+    }
+}
+
+void SnippetPanel::updateSnippetList()
+{
+    m_snippetList->clear();
+
+    const auto &snippets = SnippetMgr::getInst().getSnippets();
+    for (const auto &snippet : snippets) {
+        auto item = new QListWidgetItem(m_snippetList);
+        QString suffix;
+        if (snippet->isReadOnly()) {
+            suffix = QLatin1Char('*');
+        }
+        if (snippet->getShortcut() == Snippet::InvalidShortcut) {
+            item->setText(snippet->getName() + suffix);
+        } else {
+            item->setText(tr("%1%2 [%3]").arg(snippet->getName(), suffix, snippet->getShortcutString()));
+        }
+
+        item->setData(Qt::UserRole, snippet->getName());
+    }
+
+    updateItemsCountLabel();
+}
+
+void SnippetPanel::showEvent(QShowEvent *p_event)
+{
+    QFrame::showEvent(p_event);
+
+    if (!m_listInitialized) {
+        m_listInitialized = true;
+        updateSnippetList();
+    }
+}
+
+void SnippetPanel::handleContextMenuRequested(QPoint p_pos)
+{
+    QMenu menu(this);
+
+    auto item = m_snippetList->itemAt(p_pos);
+    if (!item) {
+        return;
+    }
+
+    const int selectedCount = m_snippetList->selectedItems().size();
+    if (selectedCount == 1) {
+        menu.addAction(tr("&Apply"),
+                       &menu,
+                       [this]() {
+                           applySnippet(m_snippetList->currentItem());
+                       });
+    }
+
+    menu.addAction(tr("&Delete"),
+                   this,
+                   &SnippetPanel::removeSelectedSnippets);
+
+    if (selectedCount == 1) {
+        menu.addAction(tr("&Properties (Rename)"),
+                       &menu,
+                       [this]() {
+                           auto item = m_snippetList->currentItem();
+                           if (!item) {
+                               return;
+                           }
+
+                           auto snippet = SnippetMgr::getInst().find(getSnippetName(item));
+                           if (!snippet) {
+                               qWarning() << "failed to find snippet for properties" << getSnippetName(item);
+                               return;
+                           }
+
+                           SnippetPropertiesDialog dialog(snippet.data(), VNoteX::getInst().getMainWindow());
+                           if (dialog.exec()) {
+                               updateSnippetList();
+                           }
+                       });
+    }
+
+    menu.exec(m_snippetList->mapToGlobal(p_pos));
+}
+
+QString SnippetPanel::getSnippetName(const QListWidgetItem *p_item)
+{
+    return p_item->data(Qt::UserRole).toString();
+}
+
+void SnippetPanel::removeSelectedSnippets()
+{
+    const auto selectedItems = m_snippetList->selectedItems();
+    if (selectedItems.isEmpty()) {
+        return;
+    }
+
+    QVector<ConfirmItemInfo> items;
+    for (const auto &selectedItem : selectedItems) {
+        const auto name = getSnippetName(selectedItem);
+        items.push_back(ConfirmItemInfo(name,
+                                        name,
+                                        QString(),
+                                        nullptr));
+    }
+
+    DeleteConfirmDialog dialog(tr("Confirm Deletion"),
+                               tr("Delete these snippets permanently?"),
+                               tr("Files will be deleted permanently and could not be found even "
+                                  "in operating system's recycle bin."),
+                               items,
+                               DeleteConfirmDialog::Flag::None,
+                               false,
+                               VNoteX::getInst().getMainWindow());
+
+    QStringList snippetsToDelete;
+    if (dialog.exec()) {
+        items = dialog.getConfirmedItems();
+        for (const auto &item : items) {
+            snippetsToDelete << item.m_name;
+        }
+    }
+
+    if (snippetsToDelete.isEmpty()) {
+        return;
+    }
+
+    for (const auto &snippetName : snippetsToDelete) {
+        try {
+            SnippetMgr::getInst().removeSnippet(snippetName);
+        } catch (Exception &p_e) {
+            QString msg = tr("Failed to remove snippet (%1) (%2).").arg(snippetName, p_e.what());
+            qCritical() << msg;
+            MessageBoxHelper::notify(MessageBoxHelper::Critical, msg, VNoteX::getInst().getMainWindow());
+        }
+    }
+
+    updateSnippetList();
+}
+
+void SnippetPanel::applySnippet(const QListWidgetItem *p_item)
+{
+    if (!p_item) {
+        return;
+    }
+    const auto name = getSnippetName(p_item);
+    if (!name.isEmpty()) {
+        emit applySnippetRequested(name);
+    }
+}

+ 53 - 0
src/widgets/snippetpanel.h

@@ -0,0 +1,53 @@
+#ifndef SNIPPETPANEL_H
+#define SNIPPETPANEL_H
+
+#include <QFrame>
+
+class QListWidget;
+class QListWidgetItem;
+
+namespace vnotex
+{
+    class TitleBar;
+
+    class SnippetPanel : public QFrame
+    {
+        Q_OBJECT
+    public:
+        explicit SnippetPanel(QWidget *p_parent = nullptr);
+
+    signals:
+        void applySnippetRequested(const QString &p_name);
+
+    protected:
+        void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE;
+
+    private slots:
+        void newSnippet();
+
+        void handleContextMenuRequested(QPoint p_pos);
+
+        void removeSelectedSnippets();
+
+        void applySnippet(const QListWidgetItem *p_item);
+
+    private:
+        void setupUI();
+
+        void setupTitleBar(const QString &p_title, QWidget *p_parent = nullptr);
+
+        void updateItemsCountLabel();
+
+        void updateSnippetList();
+
+        QString getSnippetName(const QListWidgetItem *p_item);
+
+        TitleBar *m_titleBar = nullptr;
+
+        QListWidget *m_snippetList = nullptr;
+
+        bool m_listInitialized = false;
+    };
+}
+
+#endif // SNIPPETPANEL_H

+ 14 - 0
src/widgets/textviewwindow.cpp

@@ -15,6 +15,7 @@
 #include <core/thememgr.h>
 #include <core/thememgr.h>
 #include "editors/statuswidget.h"
 #include "editors/statuswidget.h"
 #include <core/fileopenparameters.h>
 #include <core/fileopenparameters.h>
+#include <snippet/snippetmgr.h>
 
 
 using namespace vnotex;
 using namespace vnotex;
 
 
@@ -253,3 +254,16 @@ ViewWindowSession TextViewWindow::saveSession() const
     }
     }
     return session;
     return session;
 }
 }
+
+void TextViewWindow::applySnippet(const QString &p_name)
+{
+    if (m_editor->isReadOnly()) {
+        qWarning() << "failed to apply snippet to a read-only buffer" << p_name;
+        return;
+    }
+
+    m_editor->enterInsertModeIfApplicable();
+    SnippetMgr::getInst().applySnippet(p_name,
+                                       m_editor->getTextEdit(),
+                                       SnippetMgr::generateOverrides(getBuffer()));
+}

+ 2 - 0
src/widgets/textviewwindow.h

@@ -31,6 +31,8 @@ namespace vnotex
 
 
         ViewWindowSession saveSession() const Q_DECL_OVERRIDE;
         ViewWindowSession saveSession() const Q_DECL_OVERRIDE;
 
 
+        void applySnippet(const QString &p_name) Q_DECL_OVERRIDE;
+
     public slots:
     public slots:
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
 
 

+ 1 - 1
src/widgets/titlebar.cpp

@@ -180,7 +180,7 @@ QToolButton *TitleBar::addActionButton(const QString &p_iconName, const QString
     connect(p_menu, &QMenu::aboutToHide,
     connect(p_menu, &QMenu::aboutToHide,
             this, [this]() {
             this, [this]() {
                 m_actionButtonsForcedShown = false;
                 m_actionButtonsForcedShown = false;
-                setActionButtonsVisible(false);
+                setActionButtonsVisible(m_actionButtonsAlwaysShown);
             });
             });
     return btn;
     return btn;
 }
 }

+ 2 - 2
src/widgets/viewarea.h

@@ -72,13 +72,13 @@ namespace vnotex
         // Not all Workspace. Just all ViewSplits.
         // Not all Workspace. Just all ViewSplits.
         QList<Buffer *> getAllBuffersInViewSplits() const;
         QList<Buffer *> getAllBuffersInViewSplits() const;
 
 
+        ViewWindow *getCurrentViewWindow() const;
+
     public slots:
     public slots:
         void openBuffer(Buffer *p_buffer, const QSharedPointer<FileOpenParameters> &p_paras);
         void openBuffer(Buffer *p_buffer, const QSharedPointer<FileOpenParameters> &p_paras);
 
 
         bool close(const Notebook *p_notebook, bool p_force);
         bool close(const Notebook *p_notebook, bool p_force);
 
 
-        ViewWindow *getCurrentViewWindow() const;
-
         void focus();
         void focus();
 
 
     // NavigationMode.
     // NavigationMode.

+ 10 - 0
src/widgets/viewsplit.cpp

@@ -623,6 +623,16 @@ void ViewSplit::createContextMenuOnTabBar(QMenu *p_menu, int p_tabIdx) const
         WidgetUtils::addActionShortcutText(locateNodeAct,
         WidgetUtils::addActionShortcutText(locateNodeAct,
                                            ConfigMgr::getInst().getCoreConfig().getShortcut(CoreConfig::Shortcut::LocateNode));
                                            ConfigMgr::getInst().getCoreConfig().getShortcut(CoreConfig::Shortcut::LocateNode));
     }
     }
+
+    // Pin To Quick Access.
+    p_menu->addAction(tr("Pin To Quick Access"),
+                      [this, p_tabIdx]() {
+                          auto win = getViewWindow(p_tabIdx);
+                          if (win) {
+                              const QStringList files(win->getBuffer()->getPath());
+                              emit VNoteX::getInst().pinToQuickAccessRequested(files);
+                          }
+                      });
 }
 }
 
 
 void ViewSplit::closeTab(int p_idx)
 void ViewSplit::closeTab(int p_idx)

+ 2 - 0
src/widgets/viewwindow.h

@@ -84,6 +84,8 @@ namespace vnotex
         WindowFlags getWindowFlags() const;
         WindowFlags getWindowFlags() const;
         void setWindowFlags(WindowFlags p_flags);
         void setWindowFlags(WindowFlags p_flags);
 
 
+        virtual void applySnippet(const QString &p_name) = 0;
+
     public slots:
     public slots:
         virtual void handleEditorConfigChange() = 0;
         virtual void handleEditorConfigChange() = 0;
 
 

+ 4 - 0
src/widgets/viewwindowtoolbarhelper.cpp

@@ -51,6 +51,10 @@ void ViewWindowToolBarHelper::addActionShortcut(QAction *p_action,
                              }
                              }
                          }
                          }
                      });
                      });
+    QObject::connect(shortcut, &QShortcut::activatedAmbiguously,
+                     p_action, [p_action]() {
+                         qWarning() << "ViewWindow shortcut activated ambiguously" << p_action->text();
+                     });
     p_action->setText(QString("%1\t%2").arg(p_action->text(), shortcut->key().toString(QKeySequence::NativeText)));
     p_action->setText(QString("%1\t%2").arg(p_action->text(), shortcut->key().toString(QKeySequence::NativeText)));
 }
 }
 
 

+ 10 - 0
src/widgets/widgets.pri

@@ -13,6 +13,7 @@ SOURCES += \
     $$PWD/dialogs/legacynotebookutils.cpp \
     $$PWD/dialogs/legacynotebookutils.cpp \
     $$PWD/dialogs/linkinsertdialog.cpp \
     $$PWD/dialogs/linkinsertdialog.cpp \
     $$PWD/dialogs/newnotebookfromfolderdialog.cpp \
     $$PWD/dialogs/newnotebookfromfolderdialog.cpp \
+    $$PWD/dialogs/newsnippetdialog.cpp \
     $$PWD/dialogs/selectdialog.cpp \
     $$PWD/dialogs/selectdialog.cpp \
     $$PWD/dialogs/selectionitemwidget.cpp \
     $$PWD/dialogs/selectionitemwidget.cpp \
     $$PWD/dialogs/settings/appearancepage.cpp \
     $$PWD/dialogs/settings/appearancepage.cpp \
@@ -25,6 +26,8 @@ SOURCES += \
     $$PWD/dialogs/settings/settingsdialog.cpp \
     $$PWD/dialogs/settings/settingsdialog.cpp \
     $$PWD/dialogs/settings/texteditorpage.cpp \
     $$PWD/dialogs/settings/texteditorpage.cpp \
     $$PWD/dialogs/settings/themepage.cpp \
     $$PWD/dialogs/settings/themepage.cpp \
+    $$PWD/dialogs/snippetinfowidget.cpp \
+    $$PWD/dialogs/snippetpropertiesdialog.cpp \
     $$PWD/dialogs/sortdialog.cpp \
     $$PWD/dialogs/sortdialog.cpp \
     $$PWD/dialogs/tableinsertdialog.cpp \
     $$PWD/dialogs/tableinsertdialog.cpp \
     $$PWD/dragdropareaindicator.cpp \
     $$PWD/dragdropareaindicator.cpp \
@@ -47,6 +50,7 @@ SOURCES += \
     $$PWD/fullscreentoggleaction.cpp \
     $$PWD/fullscreentoggleaction.cpp \
     $$PWD/lineedit.cpp \
     $$PWD/lineedit.cpp \
     $$PWD/lineeditdelegate.cpp \
     $$PWD/lineeditdelegate.cpp \
+    $$PWD/lineeditwithsnippet.cpp \
     $$PWD/listwidget.cpp \
     $$PWD/listwidget.cpp \
     $$PWD/locationinputwithbrowsebutton.cpp \
     $$PWD/locationinputwithbrowsebutton.cpp \
     $$PWD/locationlist.cpp \
     $$PWD/locationlist.cpp \
@@ -60,6 +64,7 @@ SOURCES += \
     $$PWD/propertydefs.cpp \
     $$PWD/propertydefs.cpp \
     $$PWD/searchinfoprovider.cpp \
     $$PWD/searchinfoprovider.cpp \
     $$PWD/searchpanel.cpp \
     $$PWD/searchpanel.cpp \
+    $$PWD/snippetpanel.cpp \
     $$PWD/systemtrayhelper.cpp \
     $$PWD/systemtrayhelper.cpp \
     $$PWD/textviewwindow.cpp \
     $$PWD/textviewwindow.cpp \
     $$PWD/toolbarhelper.cpp \
     $$PWD/toolbarhelper.cpp \
@@ -112,6 +117,7 @@ HEADERS += \
     $$PWD/dialogs/legacynotebookutils.h \
     $$PWD/dialogs/legacynotebookutils.h \
     $$PWD/dialogs/linkinsertdialog.h \
     $$PWD/dialogs/linkinsertdialog.h \
     $$PWD/dialogs/newnotebookfromfolderdialog.h \
     $$PWD/dialogs/newnotebookfromfolderdialog.h \
+    $$PWD/dialogs/newsnippetdialog.h \
     $$PWD/dialogs/selectdialog.h \
     $$PWD/dialogs/selectdialog.h \
     $$PWD/dialogs/selectionitemwidget.h \
     $$PWD/dialogs/selectionitemwidget.h \
     $$PWD/dialogs/settings/appearancepage.h \
     $$PWD/dialogs/settings/appearancepage.h \
@@ -124,6 +130,8 @@ HEADERS += \
     $$PWD/dialogs/settings/settingsdialog.h \
     $$PWD/dialogs/settings/settingsdialog.h \
     $$PWD/dialogs/settings/texteditorpage.h \
     $$PWD/dialogs/settings/texteditorpage.h \
     $$PWD/dialogs/settings/themepage.h \
     $$PWD/dialogs/settings/themepage.h \
+    $$PWD/dialogs/snippetinfowidget.h \
+    $$PWD/dialogs/snippetpropertiesdialog.h \
     $$PWD/dialogs/sortdialog.h \
     $$PWD/dialogs/sortdialog.h \
     $$PWD/dialogs/tableinsertdialog.h \
     $$PWD/dialogs/tableinsertdialog.h \
     $$PWD/dragdropareaindicator.h \
     $$PWD/dragdropareaindicator.h \
@@ -146,6 +154,7 @@ HEADERS += \
     $$PWD/fullscreentoggleaction.h \
     $$PWD/fullscreentoggleaction.h \
     $$PWD/lineedit.h \
     $$PWD/lineedit.h \
     $$PWD/lineeditdelegate.h \
     $$PWD/lineeditdelegate.h \
+    $$PWD/lineeditwithsnippet.h \
     $$PWD/listwidget.h \
     $$PWD/listwidget.h \
     $$PWD/locationinputwithbrowsebutton.h \
     $$PWD/locationinputwithbrowsebutton.h \
     $$PWD/locationlist.h \
     $$PWD/locationlist.h \
@@ -160,6 +169,7 @@ HEADERS += \
     $$PWD/propertydefs.h \
     $$PWD/propertydefs.h \
     $$PWD/searchinfoprovider.h \
     $$PWD/searchinfoprovider.h \
     $$PWD/searchpanel.h \
     $$PWD/searchpanel.h \
+    $$PWD/snippetpanel.h \
     $$PWD/systemtrayhelper.h \
     $$PWD/systemtrayhelper.h \
     $$PWD/textviewwindow.h \
     $$PWD/textviewwindow.h \
     $$PWD/textviewwindowhelper.h \
     $$PWD/textviewwindowhelper.h \

+ 11 - 1
src/widgets/widgetsfactory.cpp

@@ -11,7 +11,7 @@
 #include <QPlainTextEdit>
 #include <QPlainTextEdit>
 #include <QRadioButton>
 #include <QRadioButton>
 
 
-#include "lineedit.h"
+#include "lineeditwithsnippet.h"
 #include "combobox.h"
 #include "combobox.h"
 
 
 using namespace vnotex;
 using namespace vnotex;
@@ -40,6 +40,16 @@ QLineEdit *WidgetsFactory::createLineEdit(const QString &p_contents, QWidget *p_
     return new LineEdit(p_contents, p_parent);
     return new LineEdit(p_contents, p_parent);
 }
 }
 
 
+LineEditWithSnippet *WidgetsFactory::createLineEditWithSnippet(QWidget *p_parent)
+{
+    return new LineEditWithSnippet(p_parent);
+}
+
+LineEditWithSnippet *WidgetsFactory::createLineEditWithSnippet(const QString &p_contents, QWidget *p_parent)
+{
+    return new LineEditWithSnippet(p_contents, p_parent);
+}
+
 QComboBox *WidgetsFactory::createComboBox(QWidget *p_parent)
 QComboBox *WidgetsFactory::createComboBox(QWidget *p_parent)
 {
 {
     auto comboBox = new ComboBox(p_parent);
     auto comboBox = new ComboBox(p_parent);

+ 6 - 0
src/widgets/widgetsfactory.h

@@ -16,6 +16,8 @@ class QRadioButton;
 
 
 namespace vnotex
 namespace vnotex
 {
 {
+    class LineEditWithSnippet;
+
     class WidgetsFactory
     class WidgetsFactory
     {
     {
     public:
     public:
@@ -29,6 +31,10 @@ namespace vnotex
 
 
         static QLineEdit *createLineEdit(const QString &p_contents, QWidget *p_parent = nullptr);
         static QLineEdit *createLineEdit(const QString &p_contents, QWidget *p_parent = nullptr);
 
 
+        static LineEditWithSnippet *createLineEditWithSnippet(QWidget *p_parent = nullptr);
+
+        static LineEditWithSnippet *createLineEditWithSnippet(const QString &p_contents, QWidget *p_parent = nullptr);
+
         static QComboBox *createComboBox(QWidget *p_parent = nullptr);
         static QComboBox *createComboBox(QWidget *p_parent = nullptr);
 
 
         static QCheckBox *createCheckBox(const QString &p_text, QWidget *p_parent = nullptr);
         static QCheckBox *createCheckBox(const QString &p_text, QWidget *p_parent = nullptr);

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

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