From d1d8fabb6077f1d26ec8855287432028df283117 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 25 Jun 2021 20:34:10 +0800 Subject: [PATCH] suport snippet --- libs/vtextedit | 2 +- src/core/configmgr.cpp | 9 +- src/core/configmgr.h | 2 + src/core/coreconfig.h | 1 + src/core/sessionconfig.cpp | 4 +- src/core/sessionconfig.h | 4 +- src/data/core/vnotex.json | 1 + src/data/extra/docs/en/external_programs.md | 4 +- .../extra/docs/zh_CN/external_programs.md | 4 +- src/snippet/dynamicsnippet.cpp | 30 ++ src/snippet/dynamicsnippet.h | 30 ++ src/snippet/snippet.cpp | 174 +++++++ src/snippet/snippet.h | 98 ++++ src/snippet/snippet.pri | 12 + src/snippet/snippetmgr.cpp | 433 ++++++++++++++++++ src/snippet/snippetmgr.h | 106 +++++ src/src.pro | 2 + src/utils/fileutils.cpp | 13 +- src/utils/fileutils.h | 5 + src/utils/utils.cpp | 9 + src/utils/utils.h | 2 + .../dialogs/folderpropertiesdialog.cpp | 9 +- src/widgets/dialogs/folderpropertiesdialog.h | 5 +- src/widgets/dialogs/managenotebooksdialog.cpp | 27 ++ src/widgets/dialogs/managenotebooksdialog.h | 4 + src/widgets/dialogs/newfolderdialog.cpp | 9 +- src/widgets/dialogs/newfolderdialog.h | 5 +- src/widgets/dialogs/newnotebookdialog.cpp | 9 +- src/widgets/dialogs/newnotebookdialog.h | 5 +- src/widgets/dialogs/newnotedialog.cpp | 23 +- src/widgets/dialogs/newnotedialog.h | 5 +- src/widgets/dialogs/newsnippetdialog.cpp | 88 ++++ src/widgets/dialogs/newsnippetdialog.h | 34 ++ src/widgets/dialogs/nodeinfowidget.cpp | 5 +- src/widgets/dialogs/nodeinfowidget.h | 7 +- src/widgets/dialogs/notebookinfowidget.cpp | 5 +- src/widgets/dialogs/notebookinfowidget.h | 3 +- src/widgets/dialogs/notepropertiesdialog.cpp | 17 +- src/widgets/dialogs/notepropertiesdialog.h | 5 +- .../dialogs/settings/quickaccesspage.cpp | 10 + src/widgets/dialogs/snippetinfowidget.cpp | 173 +++++++ src/widgets/dialogs/snippetinfowidget.h | 73 +++ .../dialogs/snippetpropertiesdialog.cpp | 98 ++++ src/widgets/dialogs/snippetpropertiesdialog.h | 37 ++ src/widgets/lineeditwithsnippet.cpp | 29 ++ src/widgets/lineeditwithsnippet.h | 25 + src/widgets/locationlist.cpp | 7 +- src/widgets/mainwindow.cpp | 70 ++- src/widgets/mainwindow.h | 10 +- src/widgets/markdownviewwindow.cpp | 14 + src/widgets/markdownviewwindow.h | 2 + src/widgets/notebookexplorer.cpp | 1 + src/widgets/outlineviewer.cpp | 1 + src/widgets/searchpanel.h | 2 +- src/widgets/snippetpanel.cpp | 232 ++++++++++ src/widgets/snippetpanel.h | 53 +++ src/widgets/textviewwindow.cpp | 14 + src/widgets/textviewwindow.h | 2 + src/widgets/titlebar.cpp | 2 +- src/widgets/viewarea.h | 4 +- src/widgets/viewsplit.cpp | 10 + src/widgets/viewwindow.h | 2 + src/widgets/viewwindowtoolbarhelper.cpp | 4 + src/widgets/widgets.pri | 10 + src/widgets/widgetsfactory.cpp | 12 +- src/widgets/widgetsfactory.h | 6 + .../test_core/test_notebook/test_notebook.pro | 1 + 67 files changed, 2015 insertions(+), 99 deletions(-) create mode 100644 src/snippet/dynamicsnippet.cpp create mode 100644 src/snippet/dynamicsnippet.h create mode 100644 src/snippet/snippet.cpp create mode 100644 src/snippet/snippet.h create mode 100644 src/snippet/snippet.pri create mode 100644 src/snippet/snippetmgr.cpp create mode 100644 src/snippet/snippetmgr.h create mode 100644 src/widgets/dialogs/newsnippetdialog.cpp create mode 100644 src/widgets/dialogs/newsnippetdialog.h create mode 100644 src/widgets/dialogs/snippetinfowidget.cpp create mode 100644 src/widgets/dialogs/snippetinfowidget.h create mode 100644 src/widgets/dialogs/snippetpropertiesdialog.cpp create mode 100644 src/widgets/dialogs/snippetpropertiesdialog.h create mode 100644 src/widgets/lineeditwithsnippet.cpp create mode 100644 src/widgets/lineeditwithsnippet.h create mode 100644 src/widgets/snippetpanel.cpp create mode 100644 src/widgets/snippetpanel.h diff --git a/libs/vtextedit b/libs/vtextedit index ace5699b..98274148 160000 --- a/libs/vtextedit +++ b/libs/vtextedit @@ -1 +1 @@ -Subproject commit ace5699b65e61dfaca1e252364a8a735048e115b +Subproject commit 98274148a0e1ad371f29abe072fac35bf5d7b6df diff --git a/src/core/configmgr.cpp b/src/core/configmgr.cpp index 6d111662..df08bb62 100644 --- a/src/core/configmgr.cpp +++ b/src/core/configmgr.cpp @@ -24,7 +24,7 @@ using namespace vnotex; #ifndef QT_NO_DEBUG - #define VX_DEBUG_WEB +// #define VX_DEBUG_WEB #endif const QString ConfigMgr::c_orgName = "VNote"; @@ -388,6 +388,13 @@ QString ConfigMgr::getUserTemplateFolder() const 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 { QFileInfo fi(p_filePath); diff --git a/src/core/configmgr.h b/src/core/configmgr.h index a8ba4819..448a1833 100644 --- a/src/core/configmgr.h +++ b/src/core/configmgr.h @@ -93,6 +93,8 @@ namespace vnotex QString getUserTemplateFolder() const; + QString getUserSnippetFolder() const; + // If @p_filePath is absolute, just return it. // Otherwise, first try to find it in user folder, then in app folder. QString getUserOrAppFile(const QString &p_filePath) const; diff --git a/src/core/coreconfig.h b/src/core/coreconfig.h index e827eb73..75461d38 100644 --- a/src/core/coreconfig.h +++ b/src/core/coreconfig.h @@ -24,6 +24,7 @@ namespace vnotex NavigationDock, OutlineDock, SearchDock, + SnippetDock, LocationListDock, Search, NavigationMode, diff --git a/src/core/sessionconfig.cpp b/src/core/sessionconfig.cpp index 157ddbca..c72a4de9 100644 --- a/src/core/sessionconfig.cpp +++ b/src/core/sessionconfig.cpp @@ -216,7 +216,7 @@ QJsonObject SessionConfig::saveStateAndGeometry() const QJsonObject obj; writeByteArray(obj, QStringLiteral("main_window_state"), m_mainWindowStateGeometry.m_mainState); 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; } @@ -336,7 +336,7 @@ void SessionConfig::loadStateAndGeometry(const QJsonObject &p_session) const auto obj = p_session.value(QStringLiteral("state_geometry")).toObject(); m_mainWindowStateGeometry.m_mainState = readByteArray(obj, QStringLiteral("main_window_state")); 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() diff --git a/src/core/sessionconfig.h b/src/core/sessionconfig.h index 6ca93f63..5ac62956 100644 --- a/src/core/sessionconfig.h +++ b/src/core/sessionconfig.h @@ -35,14 +35,14 @@ namespace vnotex { return m_mainState == p_other.m_mainState && m_mainGeometry == p_other.m_mainGeometry - && m_docksVisibilityBeforeExpand == p_other.m_docksVisibilityBeforeExpand; + && m_visibleDocksBeforeExpand == p_other.m_visibleDocksBeforeExpand; } QByteArray m_mainState; QByteArray m_mainGeometry; - QBitArray m_docksVisibilityBeforeExpand; + QStringList m_visibleDocksBeforeExpand; }; enum OpenGL diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index fe6669c5..1541e0a1 100644 --- a/src/data/core/vnotex.json +++ b/src/data/core/vnotex.json @@ -18,6 +18,7 @@ "NavigationDock" : "Ctrl+G, A", "OutlineDock" : "Ctrl+G, U", "SearchDock" : "Ctrl+G, S", + "SnippetDock" : "", "LocationListDock" : "Ctrl+G, L", "Search" : "Ctrl+Alt+F", "NavigationMode" : "Ctrl+G, W", diff --git a/src/data/extra/docs/en/external_programs.md b/src/data/extra/docs/en/external_programs.md index 7c44a7cb..5ba5fe3a 100644 --- a/src/data/extra/docs/en/external_programs.md +++ b/src/data/extra/docs/en/external_programs.md @@ -1,7 +1,7 @@ # External Programs 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 { @@ -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); 3. `shortcut`: the shortcut assigned to this external program; -Close VNote before editting the session configuration. +**Close VNote** before editting the session configuration. diff --git a/src/data/extra/docs/zh_CN/external_programs.md b/src/data/extra/docs/zh_CN/external_programs.md index f319ecdf..afa0ccd6 100644 --- a/src/data/extra/docs/zh_CN/external_programs.md +++ b/src/data/extra/docs/zh_CN/external_programs.md @@ -1,7 +1,7 @@ # 外部程序 VNote 支持通过在节点浏览器上下文菜单中的 `打开方式` 来调用 **外部程序** 打开笔记。 -用户需要编辑会话配置来添加自定义外部程序。一个例子如下: +用户需要编辑会话配置(用户配置文件夹下的 `session.json` 文件)来添加自定义外部程序。一个例子如下: ```json { @@ -27,4 +27,4 @@ VNote 支持通过在节点浏览器上下文菜单中的 `打开方式` 来调 1. 使用 `%1` 占位符,会被替换为真实的文件路径(自动加上双引号包裹); 3. `shortcut`: 分配给该外部程序的快捷键; -修改配置前请关闭 VNote。 +修改配置前请 **关闭 VNote** 。 diff --git a/src/snippet/dynamicsnippet.cpp b/src/snippet/dynamicsnippet.cpp new file mode 100644 index 00000000..7429bff7 --- /dev/null +++ b/src/snippet/dynamicsnippet.cpp @@ -0,0 +1,30 @@ +#include "dynamicsnippet.h" + +#include + +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; +} diff --git a/src/snippet/dynamicsnippet.h b/src/snippet/dynamicsnippet.h new file mode 100644 index 00000000..371e598c --- /dev/null +++ b/src/snippet/dynamicsnippet.h @@ -0,0 +1,30 @@ +#ifndef DYNAMICSNIPPET_H +#define DYNAMICSNIPPET_H + +#include "snippet.h" + +#include + +namespace vnotex +{ + // Snippet based on function. + // To replace the legacy Magic Word. + class DynamicSnippet : public Snippet + { + public: + typedef std::function 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 diff --git a/src/snippet/snippet.cpp b/src/snippet/snippet.cpp new file mode 100644 index 00000000..37232e97 --- /dev/null +++ b/src/snippet/snippet.cpp @@ -0,0 +1,174 @@ +#include "snippet.h" + +#include + +#include + +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(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(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; +} diff --git a/src/snippet/snippet.h b/src/snippet/snippet.h new file mode 100644 index 00000000..99fc0213 --- /dev/null +++ b/src/snippet/snippet.h @@ -0,0 +1,98 @@ +#ifndef SNIPPET_H +#define SNIPPET_H + +#include +#include + +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 diff --git a/src/snippet/snippet.pri b/src/snippet/snippet.pri new file mode 100644 index 00000000..af4ba2fb --- /dev/null +++ b/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 + diff --git a/src/snippet/snippetmgr.cpp b/src/snippet/snippetmgr.cpp new file mode 100644 index 00000000..00cfcb4a --- /dev/null +++ b/src/snippet/snippetmgr.cpp @@ -0,0 +1,433 @@ +#include "snippetmgr.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace vnotex; + +const QString SnippetMgr::c_snippetSymbolRegExp = QString("%([^%]+)%"); + +SnippetMgr::SnippetMgr() +{ + loadSnippets(); +} + +QString SnippetMgr::getSnippetFolder() const +{ + return ConfigMgr::getInst().getUserSnippetFolder(); +} + +const QVector> &SnippetMgr::getSnippets() const +{ + return m_snippets; +} + +void SnippetMgr::loadSnippets() +{ + Q_ASSERT(m_snippets.isEmpty()); + + auto builtInSnippets = loadBuiltInSnippets(); + + QSet 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 SnippetMgr::getAvailableShortcuts(int p_exemption) const +{ + QVector shortcuts; + + for (int i = 0; i < 100; ++i) { + if (!m_shortcutToSnippet.contains(i) || i == p_exemption) { + shortcuts.push_back(i); + } + } + + return shortcuts; +} + +QSharedPointer 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 &p_snippet) +{ + Q_ASSERT(!find(p_snippet->getName(), Qt::CaseInsensitive)); + saveSnippet(p_snippet); + addOneSnippet(p_snippet); +} + +void SnippetMgr::addOneSnippet(const QSharedPointer &p_snippet) +{ + m_snippets.push_back(p_snippet); + addSnippetToShortcutMap(p_snippet); +} + +QSharedPointer SnippetMgr::loadSnippet(const QString &p_snippetFile) const +{ + const auto obj = FileUtils::readJsonFile(p_snippetFile); + auto snip = QSharedPointer::create(QFileInfo(p_snippetFile).completeBaseName()); + snip->fromJson(obj); + return snip; +} + +void SnippetMgr::saveSnippet(const QSharedPointer &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 &p_snippet) const +{ + return PathUtils::concatenateFilePath(getSnippetFolder(), p_snippet->getName() + QStringLiteral(".json")); +} + +void SnippetMgr::updateSnippet(const QString &p_name, const QSharedPointer &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 &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 &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> SnippetMgr::loadBuiltInSnippets() const +{ + QVector> 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> &p_snippets, + const QString &p_name, + const QString &p_description, + const DynamicSnippet::Callback &p_callback) +{ + auto snippet = QSharedPointer::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; +} diff --git a/src/snippet/snippetmgr.h b/src/snippet/snippetmgr.h new file mode 100644 index 00000000..0e34b730 --- /dev/null +++ b/src/snippet/snippetmgr.h @@ -0,0 +1,106 @@ +#ifndef SNIPPETMGR_H +#define SNIPPETMGR_H + +#include +#include +#include +#include + +#include + +#include "dynamicsnippet.h" + +namespace vte +{ + class VTextEdit; +} + +namespace vnotex +{ + class Buffer; + + class SnippetMgr : public QObject, private Noncopyable + { + Q_OBJECT + public: + typedef QMap OverrideMap; + + static SnippetMgr &getInst() + { + static SnippetMgr inst; + return inst; + } + + QString getSnippetFolder() const; + + const QVector> &getSnippets() const; + + // @p_exemption: include it even it is occupied by one snippet. + QVector getAvailableShortcuts(int p_exemption = Snippet::InvalidShortcut) const; + + QSharedPointer find(const QString &p_name, Qt::CaseSensitivity p_cs = Qt::CaseSensitive) const; + + void addSnippet(const QSharedPointer &p_snippet); + + void removeSnippet(const QString &p_name); + + void updateSnippet(const QString &p_name, const QSharedPointer &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 loadSnippet(const QString &p_snippetFile) const; + + void saveSnippet(const QSharedPointer &p_snippet); + + QString getSnippetFile(const QSharedPointer &p_snippet) const; + + void addOneSnippet(const QSharedPointer &p_snippet); + + void removeSnippetFromShortcutMap(const QSharedPointer &p_snippet); + + void addSnippetToShortcutMap(const QSharedPointer &p_snippet); + + QVector> loadBuiltInSnippets() const; + + static void addDynamicSnippet(QVector> &p_snippets, + const QString &p_name, + const QString &p_description, + const DynamicSnippet::Callback &p_callback); + + QVector> m_snippets; + + QMap> m_shortcutToSnippet; + }; +} + +#endif // SNIPPETMGR_H diff --git a/src/src.pro b/src/src.pro index 972c50e6..c82f4ded 100644 --- a/src/src.pro +++ b/src/src.pro @@ -49,6 +49,8 @@ include($$PWD/export/export.pri) include($$PWD/search/search.pri) +include($$PWD/snippet/snippet.pri) + include($$PWD/core/core.pri) include($$PWD/widgets/widgets.pri) diff --git a/src/utils/fileutils.cpp b/src/utils/fileutils.cpp index 99b0a18d..bf041c7a 100644 --- a/src/utils/fileutils.cpp +++ b/src/utils/fileutils.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "../core/exception.h" #include "pathutils.h" @@ -34,6 +35,11 @@ QString FileUtils::readTextFile(const QString &p_filePath) 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) { QFile file(p_filePath); @@ -59,6 +65,11 @@ void FileUtils::writeFile(const QString &p_filePath, const QString &p_text) 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) { Q_ASSERT(PathUtils::isLegalFileName(p_name)); @@ -194,7 +205,7 @@ QString FileUtils::renameIfExistsCaseInsensitive(const QString &p_path) 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); if (!file.remove()) { Exception::throwOne(Exception::Type::FailToRemoveFile, diff --git a/src/utils/fileutils.h b/src/utils/fileutils.h index eb747733..01657bc2 100644 --- a/src/utils/fileutils.h +++ b/src/utils/fileutils.h @@ -5,6 +5,7 @@ #include #include #include +#include class QTemporaryFile; @@ -19,10 +20,14 @@ namespace vnotex 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 QString &p_text); + static void writeFile(const QString &p_filePath, const QJsonObject &p_jobj); + // Rename file or dir. static void renameFile(const QString &p_path, const QString &p_name); diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 9c0dfb2c..61466ce7 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -117,3 +117,12 @@ QString Utils::boolToString(bool p_val) { 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; +} diff --git a/src/utils/utils.h b/src/utils/utils.h index f5cd46e6..bf2f1822 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -50,6 +50,8 @@ namespace vnotex static bool fuzzyEqual(qreal p_a, qreal p_b); static QString boolToString(bool p_val); + + static QString intToString(int p_val, int p_width = 0); }; } // ns vnotex diff --git a/src/widgets/dialogs/folderpropertiesdialog.cpp b/src/widgets/dialogs/folderpropertiesdialog.cpp index b248c2e7..e947d8fd 100644 --- a/src/widgets/dialogs/folderpropertiesdialog.cpp +++ b/src/widgets/dialogs/folderpropertiesdialog.cpp @@ -30,7 +30,6 @@ void FolderPropertiesDialog::setupUI() setCentralWidget(m_infoWidget); setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - setButtonEnabled(QDialogButtonBox::Ok, false); setWindowTitle(m_node->getName() + QStringLiteral(" ") + tr("Properties")); } @@ -38,11 +37,9 @@ void FolderPropertiesDialog::setupUI() void FolderPropertiesDialog::setupNodeInfoWidget(QWidget *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; QString msg; @@ -50,7 +47,7 @@ void FolderPropertiesDialog::validateInputs() valid = valid && validateNameInput(msg); setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info : ScrollDialog::InformationLevel::Error); - setButtonEnabled(QDialogButtonBox::Ok, valid); + return valid; } bool FolderPropertiesDialog::validateNameInput(QString &p_msg) @@ -74,7 +71,7 @@ bool FolderPropertiesDialog::validateNameInput(QString &p_msg) void FolderPropertiesDialog::acceptedButtonClicked() { - if (saveFolderProperties()) { + if (validateInputs() && saveFolderProperties()) { accept(); } } diff --git a/src/widgets/dialogs/folderpropertiesdialog.h b/src/widgets/dialogs/folderpropertiesdialog.h index c145f585..68aa55b3 100644 --- a/src/widgets/dialogs/folderpropertiesdialog.h +++ b/src/widgets/dialogs/folderpropertiesdialog.h @@ -17,9 +17,6 @@ namespace vnotex protected: void acceptedButtonClicked() Q_DECL_OVERRIDE; - private slots: - void validateInputs(); - private: void setupUI(); @@ -29,6 +26,8 @@ namespace vnotex bool saveFolderProperties(); + bool validateInputs(); + NodeInfoWidget *m_infoWidget = nullptr; Node *m_node = nullptr; diff --git a/src/widgets/dialogs/managenotebooksdialog.cpp b/src/widgets/dialogs/managenotebooksdialog.cpp index 0e8b6ecf..e5021048 100644 --- a/src/widgets/dialogs/managenotebooksdialog.cpp +++ b/src/widgets/dialogs/managenotebooksdialog.cpp @@ -14,6 +14,7 @@ #include "notebookmgr.h" #include "../messageboxhelper.h" #include +#include #include #include "../widgetsfactory.h" #include "exception.h" @@ -212,12 +213,38 @@ void ManageNotebooksDialog::setChangesUnsaved(bool p_unsaved) 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() { if (!m_changesUnsaved || !m_notebook) { return true; } + if (!validateInputs()) { + return false; + } + m_notebook->updateName(m_notebookInfoWidget->getName()); m_notebook->updateDescription(m_notebookInfoWidget->getDescription()); return true; diff --git a/src/widgets/dialogs/managenotebooksdialog.h b/src/widgets/dialogs/managenotebooksdialog.h index 1aed63f4..d3eda054 100644 --- a/src/widgets/dialogs/managenotebooksdialog.h +++ b/src/widgets/dialogs/managenotebooksdialog.h @@ -51,6 +51,10 @@ namespace vnotex bool checkUnsavedChanges(); + bool validateInputs(); + + bool validateNameInput(QString &p_msg); + QListWidget *m_notebookList = nullptr; NotebookInfoWidget *m_notebookInfoWidget = nullptr; diff --git a/src/widgets/dialogs/newfolderdialog.cpp b/src/widgets/dialogs/newfolderdialog.cpp index 430e7fee..8e9f7d63 100644 --- a/src/widgets/dialogs/newfolderdialog.cpp +++ b/src/widgets/dialogs/newfolderdialog.cpp @@ -26,7 +26,6 @@ void NewFolderDialog::setupUI(const Node *p_node) setCentralWidget(m_infoWidget); setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - setButtonEnabled(QDialogButtonBox::Ok, false); 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) { 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; QString msg; @@ -46,7 +43,7 @@ void NewFolderDialog::validateInputs() valid = valid && validateNameInput(msg); setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info : ScrollDialog::InformationLevel::Error); - setButtonEnabled(QDialogButtonBox::Ok, valid); + return valid; } bool NewFolderDialog::validateNameInput(QString &p_msg) @@ -69,7 +66,7 @@ bool NewFolderDialog::validateNameInput(QString &p_msg) void NewFolderDialog::acceptedButtonClicked() { - if (newFolder()) { + if (validateInputs() && newFolder()) { accept(); } } diff --git a/src/widgets/dialogs/newfolderdialog.h b/src/widgets/dialogs/newfolderdialog.h index 00ff00ed..352714c5 100644 --- a/src/widgets/dialogs/newfolderdialog.h +++ b/src/widgets/dialogs/newfolderdialog.h @@ -20,9 +20,6 @@ namespace vnotex protected: void acceptedButtonClicked() Q_DECL_OVERRIDE; - private slots: - void validateInputs(); - private: void setupUI(const Node *p_node); @@ -32,6 +29,8 @@ namespace vnotex bool newFolder(); + bool validateInputs(); + NodeInfoWidget *m_infoWidget = nullptr; QSharedPointer m_newNode; diff --git a/src/widgets/dialogs/newnotebookdialog.cpp b/src/widgets/dialogs/newnotebookdialog.cpp index 7d2ac7a3..01672d78 100644 --- a/src/widgets/dialogs/newnotebookdialog.cpp +++ b/src/widgets/dialogs/newnotebookdialog.cpp @@ -29,7 +29,6 @@ void NewNotebookDialog::setupUI() setCentralWidget(m_infoWidget); setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - setButtonEnabled(QDialogButtonBox::Ok, false); setWindowTitle(tr("New Notebook")); } @@ -39,8 +38,6 @@ void NewNotebookDialog::setupNotebookInfoWidget(QWidget *p_parent) m_infoWidget = new NotebookInfoWidget(NotebookInfoWidget::Create, p_parent); connect(m_infoWidget, &NotebookInfoWidget::rootFolderEdited, this, &NewNotebookDialog::handleRootFolderPathChanged); - connect(m_infoWidget, &NotebookInfoWidget::basicInfoEdited, - this, &NewNotebookDialog::validateInputs); { auto whatsThis = tr("
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; QString msg; @@ -59,7 +56,7 @@ void NewNotebookDialog::validateInputs() setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info : ScrollDialog::InformationLevel::Error); - setButtonEnabled(QDialogButtonBox::Ok, valid); + return valid; } bool NewNotebookDialog::validateNameInput(QString &p_msg) @@ -113,7 +110,7 @@ bool NewNotebookDialog::validateRootFolderInput(QString &p_msg) void NewNotebookDialog::acceptedButtonClicked() { - if (newNotebook()) { + if (validateInputs() && newNotebook()) { accept(); } } diff --git a/src/widgets/dialogs/newnotebookdialog.h b/src/widgets/dialogs/newnotebookdialog.h index 02cc6d9f..90659884 100644 --- a/src/widgets/dialogs/newnotebookdialog.h +++ b/src/widgets/dialogs/newnotebookdialog.h @@ -22,14 +22,13 @@ namespace vnotex NotebookInfoWidget *m_infoWidget = nullptr; - private slots: - void validateInputs(); - private: void setupUI(); void setupNotebookInfoWidget(QWidget *p_parent = nullptr); + bool validateInputs(); + bool validateNameInput(QString &p_msg); // Create a new notebook. diff --git a/src/widgets/dialogs/newnotedialog.cpp b/src/widgets/dialogs/newnotedialog.cpp index e0311381..ad151c05 100644 --- a/src/widgets/dialogs/newnotedialog.cpp +++ b/src/widgets/dialogs/newnotedialog.cpp @@ -16,6 +16,7 @@ #include "nodeinfowidget.h" #include #include +#include using namespace vnotex; @@ -62,7 +63,6 @@ void NewNoteDialog::setupUI(const Node *p_node) } setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - setButtonEnabled(QDialogButtonBox::Ok, false); 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) { 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; QString msg; @@ -82,7 +80,7 @@ void NewNoteDialog::validateInputs() valid = valid && validateNameInput(msg); setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info : ScrollDialog::InformationLevel::Error); - setButtonEnabled(QDialogButtonBox::Ok, valid); + return valid; } bool NewNoteDialog::validateNameInput(QString &p_msg) @@ -90,8 +88,8 @@ bool NewNoteDialog::validateNameInput(QString &p_msg) p_msg.clear(); 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; } @@ -107,7 +105,7 @@ void NewNoteDialog::acceptedButtonClicked() { s_lastTemplate = m_templateComboBox->currentData().toString(); - if (newNote()) { + if (validateInputs() && newNote()) { accept(); } } @@ -151,8 +149,6 @@ void NewNoteDialog::initDefaultValues(const Node *p_node) QStringLiteral("md")); lineEdit->setText(defaultName); WidgetUtils::selectBaseName(lineEdit); - - validateInputs(); } } @@ -209,6 +205,9 @@ void NewNoteDialog::setupTemplateComboBox(QWidget *p_parent) 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())); } diff --git a/src/widgets/dialogs/newnotedialog.h b/src/widgets/dialogs/newnotedialog.h index d5d78477..b36d3770 100644 --- a/src/widgets/dialogs/newnotedialog.h +++ b/src/widgets/dialogs/newnotedialog.h @@ -24,9 +24,6 @@ namespace vnotex protected: void acceptedButtonClicked() Q_DECL_OVERRIDE; - private slots: - void validateInputs(); - private: void setupUI(const Node *p_node); @@ -34,6 +31,8 @@ namespace vnotex void setupTemplateComboBox(QWidget *p_parent); + bool validateInputs(); + bool validateNameInput(QString &p_msg); bool newNote(); diff --git a/src/widgets/dialogs/newsnippetdialog.cpp b/src/widgets/dialogs/newsnippetdialog.cpp new file mode 100644 index 00000000..e0cbc166 --- /dev/null +++ b/src/widgets/dialogs/newsnippetdialog.cpp @@ -0,0 +1,88 @@ +#include "newsnippetdialog.h" + +#include "snippetinfowidget.h" + +#include +#include + +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::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; +} diff --git a/src/widgets/dialogs/newsnippetdialog.h b/src/widgets/dialogs/newsnippetdialog.h new file mode 100644 index 00000000..5a7fc757 --- /dev/null +++ b/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 diff --git a/src/widgets/dialogs/nodeinfowidget.cpp b/src/widgets/dialogs/nodeinfowidget.cpp index 0d2c4b62..96ce462e 100644 --- a/src/widgets/dialogs/nodeinfowidget.cpp +++ b/src/widgets/dialogs/nodeinfowidget.cpp @@ -13,6 +13,7 @@ #include "nodelabelwithupbutton.h" #include #include +#include "../lineeditwithsnippet.h" using namespace vnotex; @@ -69,7 +70,7 @@ void NodeInfoWidget::setupUI(const Node *p_parentNode, Node::Flags p_newNodeFlag 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), m_nameLineEdit); m_nameLineEdit->setValidator(validator); @@ -107,7 +108,7 @@ QLineEdit *NodeInfoWidget::getNameLineEdit() const QString NodeInfoWidget::getName() const { - return getNameLineEdit()->text().trimmed(); + return m_nameLineEdit->evaluatedText().trimmed(); } const Notebook *NodeInfoWidget::getNotebook() const diff --git a/src/widgets/dialogs/nodeinfowidget.h b/src/widgets/dialogs/nodeinfowidget.h index 3dbbf7cf..c044a90d 100644 --- a/src/widgets/dialogs/nodeinfowidget.h +++ b/src/widgets/dialogs/nodeinfowidget.h @@ -3,7 +3,7 @@ #include -#include "notebook/node.h" +#include class QLineEdit; class QLabel; @@ -14,6 +14,7 @@ namespace vnotex { class Notebook; class NodeLabelWithUpButton; + class LineEditWithSnippet; class NodeInfoWidget : public QWidget { @@ -50,13 +51,13 @@ namespace vnotex void setNode(const Node *p_node); - Mode m_mode; + Mode m_mode = Mode::Create; QFormLayout *m_mainLayout = nullptr; QComboBox *m_fileTypeComboBox = nullptr; - QLineEdit *m_nameLineEdit = nullptr; + LineEditWithSnippet *m_nameLineEdit = nullptr; NodeLabelWithUpButton *m_parentNodeLabel = nullptr; diff --git a/src/widgets/dialogs/notebookinfowidget.cpp b/src/widgets/dialogs/notebookinfowidget.cpp index 15088ab9..e1ba05c8 100644 --- a/src/widgets/dialogs/notebookinfowidget.cpp +++ b/src/widgets/dialogs/notebookinfowidget.cpp @@ -18,6 +18,7 @@ #include #include "exception.h" #include +#include "../lineeditwithsnippet.h" 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")); connect(m_nameLineEdit, &QLineEdit::textEdited, this, &NotebookInfoWidget::basicInfoEdited); @@ -312,7 +313,7 @@ QComboBox *NotebookInfoWidget::getBackendComboBox() const QString NotebookInfoWidget::getName() const { - return getNameLineEdit()->text().trimmed(); + return m_nameLineEdit->evaluatedText().trimmed(); } QString NotebookInfoWidget::getDescription() const diff --git a/src/widgets/dialogs/notebookinfowidget.h b/src/widgets/dialogs/notebookinfowidget.h index 4c4a2aca..124e3a18 100644 --- a/src/widgets/dialogs/notebookinfowidget.h +++ b/src/widgets/dialogs/notebookinfowidget.h @@ -11,6 +11,7 @@ class QGroupBox; namespace vnotex { class Notebook; + class LineEditWithSnippet; class NotebookInfoWidget : public QWidget { @@ -92,7 +93,7 @@ namespace vnotex const Notebook *m_notebook = nullptr; - QLineEdit *m_nameLineEdit = nullptr; + LineEditWithSnippet *m_nameLineEdit = nullptr; QLineEdit *m_descriptionLineEdit = nullptr; diff --git a/src/widgets/dialogs/notepropertiesdialog.cpp b/src/widgets/dialogs/notepropertiesdialog.cpp index a5857359..5a614166 100644 --- a/src/widgets/dialogs/notepropertiesdialog.cpp +++ b/src/widgets/dialogs/notepropertiesdialog.cpp @@ -1,7 +1,5 @@ #include "notepropertiesdialog.h" -#include - #include "notebook/notebook.h" #include "notebook/node.h" #include "../widgetsfactory.h" @@ -32,19 +30,16 @@ void NotePropertiesDialog::setupUI() setCentralWidget(m_infoWidget); 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) { 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; QString msg; @@ -52,7 +47,7 @@ void NotePropertiesDialog::validateInputs() valid = valid && validateNameInput(msg); setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info : ScrollDialog::InformationLevel::Error); - setButtonEnabled(QDialogButtonBox::Ok, valid); + return valid; } bool NotePropertiesDialog::validateNameInput(QString &p_msg) @@ -60,8 +55,8 @@ bool NotePropertiesDialog::validateNameInput(QString &p_msg) p_msg.clear(); 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; } @@ -76,7 +71,7 @@ bool NotePropertiesDialog::validateNameInput(QString &p_msg) void NotePropertiesDialog::acceptedButtonClicked() { - if (saveNoteProperties()) { + if (validateInputs() && saveNoteProperties()) { accept(); } } diff --git a/src/widgets/dialogs/notepropertiesdialog.h b/src/widgets/dialogs/notepropertiesdialog.h index 728ea949..d57400f8 100644 --- a/src/widgets/dialogs/notepropertiesdialog.h +++ b/src/widgets/dialogs/notepropertiesdialog.h @@ -17,9 +17,6 @@ namespace vnotex protected: void acceptedButtonClicked() Q_DECL_OVERRIDE; - private slots: - void validateInputs(); - private: void setupUI(); @@ -29,6 +26,8 @@ namespace vnotex bool saveNoteProperties(); + bool validateInputs(); + NodeInfoWidget *m_infoWidget = nullptr; Node *m_node = nullptr; diff --git a/src/widgets/dialogs/settings/quickaccesspage.cpp b/src/widgets/dialogs/settings/quickaccesspage.cpp index 994bf10b..7a58b962 100644 --- a/src/widgets/dialogs/settings/quickaccesspage.cpp +++ b/src/widgets/dialogs/settings/quickaccesspage.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -79,6 +80,15 @@ QGroupBox *QuickAccessPage::setupFlashPageGroup() addSearchItem(label, m_flashPageInput->toolTip(), m_flashPageInput); connect(m_flashPageInput, &LocationInputWithBrowseButton::textChanged, 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; diff --git a/src/widgets/dialogs/snippetinfowidget.cpp b/src/widgets/dialogs/snippetinfowidget.cpp new file mode 100644 index 00000000..92146e34 --- /dev/null +++ b/src/widgets/dialogs/snippetinfowidget.cpp @@ -0,0 +1,173 @@ +#include "snippetinfowidget.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +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(Snippet::Type::Text)); + if (m_mode == Mode::Edit) { + m_typeComboBox->addItem(tr("Dynamic"), static_cast(Snippet::Type::Dynamic)); + } + connect(m_typeComboBox, QOverload::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::of(&QComboBox::currentIndexChanged), + this, &SnippetInfoWidget::inputEdited); +} + +QString SnippetInfoWidget::getName() const +{ + return m_nameLineEdit->text(); +} + +Snippet::Type SnippetInfoWidget::getType() const +{ + return static_cast(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(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(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); + } +} diff --git a/src/widgets/dialogs/snippetinfowidget.h b/src/widgets/dialogs/snippetinfowidget.h new file mode 100644 index 00000000..03ac2fc2 --- /dev/null +++ b/src/widgets/dialogs/snippetinfowidget.h @@ -0,0 +1,73 @@ +#ifndef SNIPPETINFOWIDGET_H +#define SNIPPETINFOWIDGET_H + +#include + +#include + +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 diff --git a/src/widgets/dialogs/snippetpropertiesdialog.cpp b/src/widgets/dialogs/snippetpropertiesdialog.cpp new file mode 100644 index 00000000..fc61040b --- /dev/null +++ b/src/widgets/dialogs/snippetpropertiesdialog.cpp @@ -0,0 +1,98 @@ +#include "snippetpropertiesdialog.h" + +#include +#include +#include + +#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::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; +} diff --git a/src/widgets/dialogs/snippetpropertiesdialog.h b/src/widgets/dialogs/snippetpropertiesdialog.h new file mode 100644 index 00000000..7719c4ca --- /dev/null +++ b/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 diff --git a/src/widgets/lineeditwithsnippet.cpp b/src/widgets/lineeditwithsnippet.cpp new file mode 100644 index 00000000..df0b0354 --- /dev/null +++ b/src/widgets/lineeditwithsnippet.cpp @@ -0,0 +1,29 @@ +#include "lineeditwithsnippet.h" + +#include + +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()); +} diff --git a/src/widgets/lineeditwithsnippet.h b/src/widgets/lineeditwithsnippet.h new file mode 100644 index 00000000..8d4b6820 --- /dev/null +++ b/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 diff --git a/src/widgets/locationlist.cpp b/src/widgets/locationlist.cpp index 9c4525c2..f4fef7c9 100644 --- a/src/widgets/locationlist.cpp +++ b/src/widgets/locationlist.cpp @@ -9,6 +9,7 @@ #include #include +#include using namespace vnotex; @@ -29,8 +30,7 @@ LocationList::LocationList(QWidget *p_parent) void LocationList::setupUI() { auto mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); + WidgetUtils::setContentsMargins(mainLayout); { setupTitleBar(QString(), this); @@ -96,6 +96,7 @@ NavigationModeWrapper *LocationList::getNavigation void LocationList::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 clearBtn = m_titleBar->addActionButton(QStringLiteral("clear.svg"), tr("Clear")); @@ -175,6 +176,6 @@ void LocationList::updateItemsCountLabel() if (cnt == 0) { m_titleBar->setInfoLabel(""); } else { - m_titleBar->setInfoLabel(tr("%n Item(s)", "", m_tree->topLevelItemCount())); + m_titleBar->setInfoLabel(tr("%n Item(s)", "", cnt)); } } diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index 4f575a42..a718705a 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -41,6 +41,7 @@ #include "titletoolbar.h" #include "locationlist.h" #include "searchpanel.h" +#include "snippetpanel.h" #include #include "searchinfoprovider.h" #include @@ -84,14 +85,15 @@ void MainWindow::kickOffOnStart(const QStringList &p_paths) VNoteX::getInst().initLoad(); + setupSpellCheck(); + + // Do necessary stuffs before emitting this signal. emit mainWindowStarted(); emit layoutChanged(); demoWidget(); - setupSpellCheck(); - openFiles(p_paths); }); } @@ -206,6 +208,8 @@ void MainWindow::setupDocks() setupSearchDock(); + setupSnippetDock(); + for (int i = 1; i < m_docks.size(); ++i) { tabifyDockWidget(m_docks[i - 1], m_docks[i]); } @@ -295,6 +299,34 @@ void MainWindow::setupSearchPanel() 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() { auto dock = new QDockWidget(tr("Location List"), this); @@ -418,7 +450,7 @@ void MainWindow::saveStateAndGeometry() SessionConfig::MainWindowStateGeometry sg; sg.m_mainState = saveState(); sg.m_mainGeometry = saveGeometry(); - sg.m_docksVisibilityBeforeExpand = m_docksVisibilityBeforeExpand; + sg.m_visibleDocksBeforeExpand = m_visibleDocksBeforeExpand; auto& sessionConfig = ConfigMgr::getInst().getSessionConfig(); sessionConfig.setMainWindowStateGeometry(sg); @@ -439,12 +471,13 @@ void MainWindow::loadStateAndGeometry(bool 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) { - 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) { // Store the state and hide. + m_visibleDocksBeforeExpand.clear(); 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; } @@ -481,15 +518,15 @@ void MainWindow::setContentAreaExpanded(bool p_expanded) // Restore the state. bool hasVisible = false; 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; } - 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) { @@ -606,6 +643,9 @@ void MainWindow::setupShortcuts() setupDockActivateShortcut(m_docks[DockIndex::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) diff --git a/src/widgets/mainwindow.h b/src/widgets/mainwindow.h index 691849c3..3256cc0a 100644 --- a/src/widgets/mainwindow.h +++ b/src/widgets/mainwindow.h @@ -22,6 +22,7 @@ namespace vnotex class OutlineViewer; class LocationList; class SearchPanel; + class SnippetPanel; enum { RESTART_EXIT_CODE = 1000 }; @@ -96,6 +97,7 @@ namespace vnotex NavigationDock = 0, OutlineDock, SearchDock, + SnippetDock, LocationListDock }; @@ -117,6 +119,10 @@ namespace vnotex void setupLocationList(); + void setupSnippetDock(); + + void setupSnippetPanel(); + void setupNotebookExplorer(QWidget *p_parent = nullptr); void setupDocks(); @@ -168,6 +174,8 @@ namespace vnotex SearchPanel *m_searchPanel = nullptr; + SnippetPanel *m_snippetPanel = nullptr; + QVector m_docks; bool m_layoutReset = false; @@ -184,7 +192,7 @@ namespace vnotex QTimer *m_tipsTimer = nullptr; - QBitArray m_docksVisibilityBeforeExpand; + QStringList m_visibleDocksBeforeExpand; }; } // ns vnotex diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 8ccc24f0..17088a6c 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -31,6 +31,7 @@ #include "editors/statuswidget.h" #include "editors/plantumlhelper.h" #include "editors/graphvizhelper.h" +#include using namespace vnotex; @@ -975,3 +976,16 @@ void MarkdownViewWindow::setupPreviewHelper() markdownEditorConfig.getPlantUmlCommand()); 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())); +} diff --git a/src/widgets/markdownviewwindow.h b/src/widgets/markdownviewwindow.h index 28b7b7ed..4a584c91 100644 --- a/src/widgets/markdownviewwindow.h +++ b/src/widgets/markdownviewwindow.h @@ -44,6 +44,8 @@ namespace vnotex ViewWindowSession saveSession() const Q_DECL_OVERRIDE; + void applySnippet(const QString &p_name) Q_DECL_OVERRIDE; + public slots: void handleEditorConfigChange() Q_DECL_OVERRIDE; diff --git a/src/widgets/notebookexplorer.cpp b/src/widgets/notebookexplorer.cpp index 8935fdc7..145ce4a3 100644 --- a/src/widgets/notebookexplorer.cpp +++ b/src/widgets/notebookexplorer.cpp @@ -105,6 +105,7 @@ TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent) TitleBar::Action::Menu, p_parent); titleBar->setWhatsThis(tr("This title bar contains buttons and menu to manage notebooks and notes.")); + titleBar->setActionButtonsAlwaysShown(true); { auto viewMenu = WidgetsFactory::createMenu(titleBar); diff --git a/src/widgets/outlineviewer.cpp b/src/widgets/outlineviewer.cpp index 3a4174b2..2b9b59f4 100644 --- a/src/widgets/outlineviewer.cpp +++ b/src/widgets/outlineviewer.cpp @@ -90,6 +90,7 @@ NavigationModeWrapper *OutlineViewer::getNavigatio TitleBar *OutlineViewer::setupTitleBar(const QString &p_title, QWidget *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")); connect(decreaseBtn, &QToolButton::clicked, diff --git a/src/widgets/searchpanel.h b/src/widgets/searchpanel.h index a7dd760a..5be06f73 100644 --- a/src/widgets/searchpanel.h +++ b/src/widgets/searchpanel.h @@ -47,7 +47,7 @@ namespace vnotex { Q_OBJECT public: - explicit SearchPanel(const QSharedPointer &p_provider, QWidget *p_parent = nullptr); + SearchPanel(const QSharedPointer &p_provider, QWidget *p_parent = nullptr); private slots: void startSearch(); diff --git a/src/widgets/snippetpanel.cpp b/src/widgets/snippetpanel.cpp new file mode 100644 index 00000000..7244f83b --- /dev/null +++ b/src/widgets/snippetpanel.cpp @@ -0,0 +1,232 @@ +#include "snippetpanel.h" + +#include +#include +#include + +#include +#include +#include +#include + +#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 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); + } +} diff --git a/src/widgets/snippetpanel.h b/src/widgets/snippetpanel.h new file mode 100644 index 00000000..9c891a58 --- /dev/null +++ b/src/widgets/snippetpanel.h @@ -0,0 +1,53 @@ +#ifndef SNIPPETPANEL_H +#define SNIPPETPANEL_H + +#include + +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 diff --git a/src/widgets/textviewwindow.cpp b/src/widgets/textviewwindow.cpp index 3c258856..a85fb8fb 100644 --- a/src/widgets/textviewwindow.cpp +++ b/src/widgets/textviewwindow.cpp @@ -15,6 +15,7 @@ #include #include "editors/statuswidget.h" #include +#include using namespace vnotex; @@ -253,3 +254,16 @@ ViewWindowSession TextViewWindow::saveSession() const } 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())); +} diff --git a/src/widgets/textviewwindow.h b/src/widgets/textviewwindow.h index ad4a4ba9..2f66fcfe 100644 --- a/src/widgets/textviewwindow.h +++ b/src/widgets/textviewwindow.h @@ -31,6 +31,8 @@ namespace vnotex ViewWindowSession saveSession() const Q_DECL_OVERRIDE; + void applySnippet(const QString &p_name) Q_DECL_OVERRIDE; + public slots: void handleEditorConfigChange() Q_DECL_OVERRIDE; diff --git a/src/widgets/titlebar.cpp b/src/widgets/titlebar.cpp index 43295fb7..50adeaa1 100644 --- a/src/widgets/titlebar.cpp +++ b/src/widgets/titlebar.cpp @@ -180,7 +180,7 @@ QToolButton *TitleBar::addActionButton(const QString &p_iconName, const QString connect(p_menu, &QMenu::aboutToHide, this, [this]() { m_actionButtonsForcedShown = false; - setActionButtonsVisible(false); + setActionButtonsVisible(m_actionButtonsAlwaysShown); }); return btn; } diff --git a/src/widgets/viewarea.h b/src/widgets/viewarea.h index 02ac46a5..803bcfbe 100644 --- a/src/widgets/viewarea.h +++ b/src/widgets/viewarea.h @@ -72,13 +72,13 @@ namespace vnotex // Not all Workspace. Just all ViewSplits. QList getAllBuffersInViewSplits() const; + ViewWindow *getCurrentViewWindow() const; + public slots: void openBuffer(Buffer *p_buffer, const QSharedPointer &p_paras); bool close(const Notebook *p_notebook, bool p_force); - ViewWindow *getCurrentViewWindow() const; - void focus(); // NavigationMode. diff --git a/src/widgets/viewsplit.cpp b/src/widgets/viewsplit.cpp index 4052522f..5883a5be 100644 --- a/src/widgets/viewsplit.cpp +++ b/src/widgets/viewsplit.cpp @@ -623,6 +623,16 @@ void ViewSplit::createContextMenuOnTabBar(QMenu *p_menu, int p_tabIdx) const WidgetUtils::addActionShortcutText(locateNodeAct, 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) diff --git a/src/widgets/viewwindow.h b/src/widgets/viewwindow.h index 75281bcc..447963c6 100644 --- a/src/widgets/viewwindow.h +++ b/src/widgets/viewwindow.h @@ -84,6 +84,8 @@ namespace vnotex WindowFlags getWindowFlags() const; void setWindowFlags(WindowFlags p_flags); + virtual void applySnippet(const QString &p_name) = 0; + public slots: virtual void handleEditorConfigChange() = 0; diff --git a/src/widgets/viewwindowtoolbarhelper.cpp b/src/widgets/viewwindowtoolbarhelper.cpp index 7487d4f2..416f3b28 100644 --- a/src/widgets/viewwindowtoolbarhelper.cpp +++ b/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))); } diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index dbe181b8..4a8a93c0 100644 --- a/src/widgets/widgets.pri +++ b/src/widgets/widgets.pri @@ -13,6 +13,7 @@ SOURCES += \ $$PWD/dialogs/legacynotebookutils.cpp \ $$PWD/dialogs/linkinsertdialog.cpp \ $$PWD/dialogs/newnotebookfromfolderdialog.cpp \ + $$PWD/dialogs/newsnippetdialog.cpp \ $$PWD/dialogs/selectdialog.cpp \ $$PWD/dialogs/selectionitemwidget.cpp \ $$PWD/dialogs/settings/appearancepage.cpp \ @@ -25,6 +26,8 @@ SOURCES += \ $$PWD/dialogs/settings/settingsdialog.cpp \ $$PWD/dialogs/settings/texteditorpage.cpp \ $$PWD/dialogs/settings/themepage.cpp \ + $$PWD/dialogs/snippetinfowidget.cpp \ + $$PWD/dialogs/snippetpropertiesdialog.cpp \ $$PWD/dialogs/sortdialog.cpp \ $$PWD/dialogs/tableinsertdialog.cpp \ $$PWD/dragdropareaindicator.cpp \ @@ -47,6 +50,7 @@ SOURCES += \ $$PWD/fullscreentoggleaction.cpp \ $$PWD/lineedit.cpp \ $$PWD/lineeditdelegate.cpp \ + $$PWD/lineeditwithsnippet.cpp \ $$PWD/listwidget.cpp \ $$PWD/locationinputwithbrowsebutton.cpp \ $$PWD/locationlist.cpp \ @@ -60,6 +64,7 @@ SOURCES += \ $$PWD/propertydefs.cpp \ $$PWD/searchinfoprovider.cpp \ $$PWD/searchpanel.cpp \ + $$PWD/snippetpanel.cpp \ $$PWD/systemtrayhelper.cpp \ $$PWD/textviewwindow.cpp \ $$PWD/toolbarhelper.cpp \ @@ -112,6 +117,7 @@ HEADERS += \ $$PWD/dialogs/legacynotebookutils.h \ $$PWD/dialogs/linkinsertdialog.h \ $$PWD/dialogs/newnotebookfromfolderdialog.h \ + $$PWD/dialogs/newsnippetdialog.h \ $$PWD/dialogs/selectdialog.h \ $$PWD/dialogs/selectionitemwidget.h \ $$PWD/dialogs/settings/appearancepage.h \ @@ -124,6 +130,8 @@ HEADERS += \ $$PWD/dialogs/settings/settingsdialog.h \ $$PWD/dialogs/settings/texteditorpage.h \ $$PWD/dialogs/settings/themepage.h \ + $$PWD/dialogs/snippetinfowidget.h \ + $$PWD/dialogs/snippetpropertiesdialog.h \ $$PWD/dialogs/sortdialog.h \ $$PWD/dialogs/tableinsertdialog.h \ $$PWD/dragdropareaindicator.h \ @@ -146,6 +154,7 @@ HEADERS += \ $$PWD/fullscreentoggleaction.h \ $$PWD/lineedit.h \ $$PWD/lineeditdelegate.h \ + $$PWD/lineeditwithsnippet.h \ $$PWD/listwidget.h \ $$PWD/locationinputwithbrowsebutton.h \ $$PWD/locationlist.h \ @@ -160,6 +169,7 @@ HEADERS += \ $$PWD/propertydefs.h \ $$PWD/searchinfoprovider.h \ $$PWD/searchpanel.h \ + $$PWD/snippetpanel.h \ $$PWD/systemtrayhelper.h \ $$PWD/textviewwindow.h \ $$PWD/textviewwindowhelper.h \ diff --git a/src/widgets/widgetsfactory.cpp b/src/widgets/widgetsfactory.cpp index 36891346..99336a0f 100644 --- a/src/widgets/widgetsfactory.cpp +++ b/src/widgets/widgetsfactory.cpp @@ -11,7 +11,7 @@ #include #include -#include "lineedit.h" +#include "lineeditwithsnippet.h" #include "combobox.h" using namespace vnotex; @@ -40,6 +40,16 @@ QLineEdit *WidgetsFactory::createLineEdit(const QString &p_contents, QWidget *p_ 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) { auto comboBox = new ComboBox(p_parent); diff --git a/src/widgets/widgetsfactory.h b/src/widgets/widgetsfactory.h index 9966d51b..9f98c126 100644 --- a/src/widgets/widgetsfactory.h +++ b/src/widgets/widgetsfactory.h @@ -16,6 +16,8 @@ class QRadioButton; namespace vnotex { + class LineEditWithSnippet; + class WidgetsFactory { public: @@ -29,6 +31,10 @@ namespace vnotex 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 QCheckBox *createCheckBox(const QString &p_text, QWidget *p_parent = nullptr); diff --git a/tests/test_core/test_notebook/test_notebook.pro b/tests/test_core/test_notebook/test_notebook.pro index 5b6140f2..22dd9b9d 100644 --- a/tests/test_core/test_notebook/test_notebook.pro +++ b/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/export/export.pri) include($$SRC_FOLDER/search/search.pri) +include($$SRC_FOLDER/snippet/snippet.pri) SOURCES += \ test_notebook.cpp