suport snippet

This commit is contained in:
Le Tan 2021-06-25 20:34:10 +08:00
parent ebd65b26be
commit d1d8fabb60
67 changed files with 2015 additions and 99 deletions

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

View File

@ -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);

View File

@ -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;

View File

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

View File

@ -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()

View File

@ -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

View File

@ -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",

View File

@ -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.

View File

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

View File

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

View File

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

174
src/snippet/snippet.cpp Normal file
View File

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

98
src/snippet/snippet.h Normal file
View File

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

12
src/snippet/snippet.pri Normal file
View File

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

433
src/snippet/snippetmgr.cpp Normal file
View File

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

106
src/snippet/snippetmgr.h Normal file
View File

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

View File

@ -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)

View File

@ -4,6 +4,7 @@
#include <QMimeDatabase>
#include <QDateTime>
#include <QTemporaryFile>
#include <QJsonDocument>
#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,

View File

@ -5,6 +5,7 @@
#include <QString>
#include <QImage>
#include <QPixmap>
#include <QJsonObject>
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);

View File

@ -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;
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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;

View File

@ -14,6 +14,7 @@
#include "notebookmgr.h"
#include "../messageboxhelper.h"
#include <utils/iconutils.h>
#include <utils/utils.h>
#include <utils/widgetutils.h>
#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;

View File

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

View File

@ -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();
}
}

View File

@ -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<Node> m_newNode;

View File

@ -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("<br/>Both absolute and relative paths are supported. ~ and environment variable are not supported now.");
@ -49,7 +46,7 @@ void NewNotebookDialog::setupNotebookInfoWidget(QWidget *p_parent)
}
}
void NewNotebookDialog::validateInputs()
bool NewNotebookDialog::validateInputs()
{
bool valid = true;
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();
}
}

View File

@ -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.

View File

@ -16,6 +16,7 @@
#include "nodeinfowidget.h"
#include <utils/widgetutils.h>
#include <core/templatemgr.h>
#include <snippet/snippetmgr.h>
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()));
}

View File

@ -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();

View File

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

View File

@ -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

View File

@ -13,6 +13,7 @@
#include "nodelabelwithupbutton.h"
#include <utils/widgetutils.h>
#include <buffer/filetypehelper.h>
#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

View File

@ -3,7 +3,7 @@
#include <QWidget>
#include "notebook/node.h"
#include <notebook/node.h>
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;

View File

@ -18,6 +18,7 @@
#include <utils/pathutils.h>
#include "exception.h"
#include <utils/widgetutils.h>
#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

View File

@ -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;

View File

@ -1,7 +1,5 @@
#include "notepropertiesdialog.h"
#include <QtWidgets>
#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();
}
}

View File

@ -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;

View File

@ -5,6 +5,7 @@
#include <QGroupBox>
#include <QPlainTextEdit>
#include <QDebug>
#include <QFileDialog>
#include <widgets/widgetsfactory.h>
#include <core/sessionconfig.h>
@ -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;

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -9,6 +9,7 @@
#include <core/vnotex.h>
#include <utils/iconutils.h>
#include <utils/widgetutils.h>
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<QTreeWidget, QTreeWidgetItem> *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));
}
}

View File

@ -41,6 +41,7 @@
#include "titletoolbar.h"
#include "locationlist.h"
#include "searchpanel.h"
#include "snippetpanel.h"
#include <notebook/notebook.h>
#include "searchinfoprovider.h"
#include <vtextedit/spellchecker.h>
@ -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)

View File

@ -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<QDockWidget *> m_docks;
bool m_layoutReset = false;
@ -184,7 +192,7 @@ namespace vnotex
QTimer *m_tipsTimer = nullptr;
QBitArray m_docksVisibilityBeforeExpand;
QStringList m_visibleDocksBeforeExpand;
};
} // ns vnotex

View File

@ -31,6 +31,7 @@
#include "editors/statuswidget.h"
#include "editors/plantumlhelper.h"
#include "editors/graphvizhelper.h"
#include <snippet/snippetmgr.h>
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()));
}

View File

@ -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;

View File

@ -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);

View File

@ -90,6 +90,7 @@ NavigationModeWrapper<QTreeWidget, QTreeWidgetItem> *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,

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@
#include <core/thememgr.h>
#include "editors/statuswidget.h"
#include <core/fileopenparameters.h>
#include <snippet/snippetmgr.h>
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()));
}

View File

@ -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;

View File

@ -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;
}

View File

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

View File

@ -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)

View File

@ -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;

View File

@ -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)));
}

View File

@ -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 \

View File

@ -11,7 +11,7 @@
#include <QPlainTextEdit>
#include <QRadioButton>
#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);

View File

@ -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);

View File

@ -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