From 6ac33d2bd00b26f4dff6479bcbc5d1a625cb3dee Mon Sep 17 00:00:00 2001 From: Le Tan Date: Tue, 7 Nov 2017 19:52:54 +0800 Subject: [PATCH] support snippets Shortcuts are not supported yet. --- src/dialog/veditsnippetdialog.cpp | 296 ++++++++++++ src/dialog/veditsnippetdialog.h | 66 +++ src/resources/icons/add_snippet.svg | 7 + src/resources/icons/apply_snippet.svg | 16 + src/resources/icons/delete_snippet.svg | 10 + src/resources/icons/locate_snippet.svg | 10 + src/resources/icons/snippet_info.svg | 10 + src/resources/icons/snippets.svg | 16 + src/src.pro | 13 +- src/utils/vimnavigationforwidget.cpp | 69 +++ src/utils/vimnavigationforwidget.h | 24 + src/utils/vutils.cpp | 45 +- src/utils/vutils.h | 13 + src/utils/vvim.cpp | 34 +- src/vattachmentlist.cpp | 46 +- src/vconfigmanager.cpp | 26 +- src/vconfigmanager.h | 11 + src/vconstants.h | 12 + src/vdirectorytree.cpp | 133 +----- src/vdirectorytree.h | 11 - src/veditarea.cpp | 3 +- src/veditarea.h | 5 - src/veditor.cpp | 8 + src/veditor.h | 3 + src/vedittab.cpp | 5 + src/vedittab.h | 4 + src/vfilelist.cpp | 126 +---- src/vfilelist.h | 12 +- src/vmainwindow.cpp | 17 +- src/vmainwindow.h | 17 +- src/vmdeditor.h | 1 - src/vmdtab.cpp | 17 + src/vmdtab.h | 2 + src/vnavigationmode.cpp | 202 +++++++++ src/vnavigationmode.h | 48 +- src/vnote.qrc | 6 + src/vnotebookselector.cpp | 43 +- src/vopenedlistmenu.cpp | 9 +- src/voutline.cpp | 100 +--- src/voutline.h | 10 - src/vsnippet.cpp | 207 +++++++++ src/vsnippet.h | 113 +++++ src/vsnippetlist.cpp | 606 +++++++++++++++++++++++++ src/vsnippetlist.h | 98 ++++ 44 files changed, 2041 insertions(+), 489 deletions(-) create mode 100644 src/dialog/veditsnippetdialog.cpp create mode 100644 src/dialog/veditsnippetdialog.h create mode 100644 src/resources/icons/add_snippet.svg create mode 100644 src/resources/icons/apply_snippet.svg create mode 100644 src/resources/icons/delete_snippet.svg create mode 100644 src/resources/icons/locate_snippet.svg create mode 100644 src/resources/icons/snippet_info.svg create mode 100644 src/resources/icons/snippets.svg create mode 100644 src/utils/vimnavigationforwidget.cpp create mode 100644 src/utils/vimnavigationforwidget.h create mode 100644 src/vnavigationmode.cpp create mode 100644 src/vsnippet.cpp create mode 100644 src/vsnippet.h create mode 100644 src/vsnippetlist.cpp create mode 100644 src/vsnippetlist.h diff --git a/src/dialog/veditsnippetdialog.cpp b/src/dialog/veditsnippetdialog.cpp new file mode 100644 index 00000000..9d56a302 --- /dev/null +++ b/src/dialog/veditsnippetdialog.cpp @@ -0,0 +1,296 @@ +#include "veditsnippetdialog.h" +#include + +#include "utils/vutils.h" +#include "vlineedit.h" +#include "vconfigmanager.h" +#include "utils/vmetawordmanager.h" + +extern VMetaWordManager *g_mwMgr; + +extern VConfigManager *g_config; + +VEditSnippetDialog::VEditSnippetDialog(const QString &p_title, + const QString &p_info, + const QVector &p_snippets, + const VSnippet &p_snippet, + QWidget *p_parent) + : QDialog(p_parent), + m_snippets(p_snippets), + m_snippet(p_snippet) +{ + setupUI(p_title, p_info); + + handleInputChanged(); +} + +void VEditSnippetDialog::setupUI(const QString &p_title, const QString &p_info) +{ + QLabel *infoLabel = NULL; + if (!p_info.isEmpty()) { + infoLabel = new QLabel(p_info); + infoLabel->setWordWrap(true); + } + + // Name. + m_nameEdit = new VLineEdit(m_snippet.getName()); + QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp), + m_nameEdit); + m_nameEdit->setValidator(validator); + + // Type. + m_typeCB = new QComboBox(); + for (int i = 0; i < VSnippet::Type::Invalid; ++i) { + m_typeCB->addItem(VSnippet::typeStr(static_cast(i)), i); + } + + int typeIdx = m_typeCB->findData((int)m_snippet.getType()); + Q_ASSERT(typeIdx > -1); + m_typeCB->setCurrentIndex(typeIdx); + + // Shortcut. + m_shortcutCB = new QComboBox(); + m_shortcutCB->addItem(tr("None"), QChar()); + auto shortcuts = getAvailableShortcuts(); + for (auto it : shortcuts) { + m_shortcutCB->addItem(it, it); + } + + QChar sh = m_snippet.getShortcut(); + if (sh.isNull()) { + m_shortcutCB->setCurrentIndex(0); + } else { + int shortcutIdx = m_shortcutCB->findData(sh); + m_shortcutCB->setCurrentIndex(shortcutIdx < 0 ? 0 : shortcutIdx); + } + + // Cursor mark. + m_cursorMarkEdit = new QLineEdit(m_snippet.getCursorMark()); + m_cursorMarkEdit->setToolTip(tr("String in the content to mark the cursor position")); + + // Selection mark. + m_selectionMarkEdit = new QLineEdit(m_snippet.getSelectionMark()); + m_selectionMarkEdit->setToolTip(tr("String in the content to be replaced with selected text")); + + // Content. + m_contentEdit = new QTextEdit(); + setContentEditByType(); + + QFormLayout *topLayout = new QFormLayout(); + topLayout->addRow(tr("Snippet &name:"), m_nameEdit); + topLayout->addRow(tr("Snippet &type:"), m_typeCB); + topLayout->addRow(tr("Shortc&ut:"), m_shortcutCB); + topLayout->addRow(tr("Cursor &mark:"), m_cursorMarkEdit); + topLayout->addRow(tr("&Selection mark:"), m_selectionMarkEdit); + topLayout->addRow(tr("&Content:"), m_contentEdit); + + m_warnLabel = new QLabel(); + m_warnLabel->setWordWrap(true); + m_warnLabel->hide(); + + // Ok is the default button. + m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(m_btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok); + m_nameEdit->setMinimumWidth(okBtn->sizeHint().width() * 3); + + QVBoxLayout *mainLayout = new QVBoxLayout(); + if (infoLabel) { + mainLayout->addWidget(infoLabel); + } + + mainLayout->addLayout(topLayout); + mainLayout->addWidget(m_warnLabel); + mainLayout->addWidget(m_btnBox); + mainLayout->setSizeConstraint(QLayout::SetFixedSize); + setLayout(mainLayout); + + setWindowTitle(p_title); + + connect(m_nameEdit, &QLineEdit::textChanged, + this, &VEditSnippetDialog::handleInputChanged); + + connect(m_typeCB, static_cast(&QComboBox::currentIndexChanged), + this, &VEditSnippetDialog::handleInputChanged); + + connect(m_cursorMarkEdit, &QLineEdit::textChanged, + this, &VEditSnippetDialog::handleInputChanged); + + connect(m_selectionMarkEdit, &QLineEdit::textChanged, + this, &VEditSnippetDialog::handleInputChanged); + + connect(m_contentEdit, &QTextEdit::textChanged, + this, &VEditSnippetDialog::handleInputChanged); +} + +void VEditSnippetDialog::handleInputChanged() +{ + bool showWarnLabel = false; + QString name = m_nameEdit->getEvaluatedText(); + bool nameOk = !name.isEmpty(); + if (nameOk && name != m_snippet.getName()) { + // Check if the name conflicts with existing snippet name. + // Case-insensitive. + QString lowerName = name.toLower(); + bool conflicted = false; + for (auto const & item : m_snippets) { + if (item.getName() == name) { + conflicted = true; + break; + } + } + + QString warnText; + if (conflicted) { + nameOk = false; + warnText = tr("WARNING: " + "Name (case-insensitive) %3 already exists. " + "Please choose another name.") + .arg(g_config->c_warningTextStyle) + .arg(g_config->c_dataTextStyle) + .arg(name); + } else if (!VUtils::checkFileNameLegal(name)) { + // Check if evaluated name contains illegal characters. + nameOk = false; + warnText = tr("WARNING: " + "Name %3 contains illegal characters " + "(after magic word evaluation).") + .arg(g_config->c_warningTextStyle) + .arg(g_config->c_dataTextStyle) + .arg(name); + } + + if (!nameOk) { + showWarnLabel = true; + m_warnLabel->setText(warnText); + } + } + + QString cursorMark = m_cursorMarkEdit->text(); + bool cursorMarkOk = true; + if (nameOk && !cursorMark.isEmpty()) { + // Check if the mark appears more than once in the content. + QString selectionMark = m_selectionMarkEdit->text(); + QString content = getContentEditByType(); + content = g_mwMgr->evaluate(content); + QString warnText; + if (content.count(cursorMark) > 1) { + cursorMarkOk = false; + warnText = tr("WARNING: " + "Cursor mark %3 occurs more than once " + "in the content (after magic word evaluation). " + "Please choose another mark.") + .arg(g_config->c_warningTextStyle) + .arg(g_config->c_dataTextStyle) + .arg(cursorMark); + } else if ((cursorMark == selectionMark + || cursorMark.contains(selectionMark) + || selectionMark.contains(cursorMark)) + && !selectionMark.isEmpty()) { + cursorMarkOk = false; + warnText = tr("WARNING: " + "Cursor mark %3 conflicts with selection mark. " + "Please choose another mark.") + .arg(g_config->c_warningTextStyle) + .arg(g_config->c_dataTextStyle) + .arg(cursorMark); + } + + if (!cursorMarkOk) { + showWarnLabel = true; + m_warnLabel->setText(warnText); + } + } + + m_warnLabel->setVisible(showWarnLabel); + + QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok); + okBtn->setEnabled(nameOk && cursorMarkOk); +} + +void VEditSnippetDialog::setContentEditByType() +{ + switch (m_snippet.getType()) { + case VSnippet::Type::PlainText: + m_contentEdit->setPlainText(m_snippet.getContent()); + break; + + case VSnippet::Type::Html: + m_contentEdit->setHtml(m_snippet.getContent()); + break; + + default: + m_contentEdit->setPlainText(m_snippet.getContent()); + break; + } +} + +QString VEditSnippetDialog::getContentEditByType() const +{ + if (m_typeCB->currentIndex() == -1) { + return QString(); + } + + switch (static_cast(m_typeCB->currentData().toInt())) { + case VSnippet::Type::PlainText: + return m_contentEdit->toPlainText(); + + case VSnippet::Type::Html: + return m_contentEdit->toHtml(); + + default: + return m_contentEdit->toPlainText(); + } +} + +QString VEditSnippetDialog::getNameInput() const +{ + return m_nameEdit->getEvaluatedText(); +} + +VSnippet::Type VEditSnippetDialog::getTypeInput() const +{ + return static_cast(m_typeCB->currentData().toInt()); +} + +QString VEditSnippetDialog::getCursorMarkInput() const +{ + return m_cursorMarkEdit->text(); +} + +QString VEditSnippetDialog::getSelectionMarkInput() const +{ + return m_selectionMarkEdit->text(); +} + +QString VEditSnippetDialog::getContentInput() const +{ + return getContentEditByType(); +} + +QChar VEditSnippetDialog::getShortcutInput() const +{ + return m_shortcutCB->currentData().toChar(); +} + +QVector VEditSnippetDialog::getAvailableShortcuts() const +{ + QVector ret = VSnippet::getAllShortcuts(); + + QChar curCh = m_snippet.getShortcut(); + // Remove those have already been assigned to snippets. + for (auto const & snip : m_snippets) { + QChar ch = snip.getShortcut(); + if (ch.isNull()) { + continue; + } + + if (ch != curCh) { + ret.removeOne(ch); + } + } + + return ret; +} diff --git a/src/dialog/veditsnippetdialog.h b/src/dialog/veditsnippetdialog.h new file mode 100644 index 00000000..cdbc1487 --- /dev/null +++ b/src/dialog/veditsnippetdialog.h @@ -0,0 +1,66 @@ +#ifndef VEDITSNIPPETDIALOG_H +#define VEDITSNIPPETDIALOG_H + +#include +#include + +#include "vsnippet.h" + +class VLineEdit; +class QLineEdit; +class QLabel; +class QDialogButtonBox; +class QComboBox; +class QTextEdit; + + +class VEditSnippetDialog : public QDialog +{ + Q_OBJECT +public: + VEditSnippetDialog(const QString &p_title, + const QString &p_info, + const QVector &p_snippets, + const VSnippet &p_snippet, + QWidget *p_parent = nullptr); + + QString getNameInput() const; + + VSnippet::Type getTypeInput() const; + + QString getCursorMarkInput() const; + + QString getSelectionMarkInput() const; + + QString getContentInput() const; + + QChar getShortcutInput() const; + +private slots: + void handleInputChanged(); + +private: + void setupUI(const QString &p_title, const QString &p_info); + + void setContentEditByType(); + + QString getContentEditByType() const; + + QVector getAvailableShortcuts() const; + + VLineEdit *m_nameEdit; + QComboBox *m_typeCB; + QComboBox *m_shortcutCB; + QLineEdit *m_cursorMarkEdit; + QLineEdit *m_selectionMarkEdit; + QTextEdit *m_contentEdit; + + QLabel *m_warnLabel; + QDialogButtonBox *m_btnBox; + + const QVector &m_snippets; + + const VSnippet &m_snippet; +}; + +#endif // VEDITSNIPPETDIALOG_H diff --git a/src/resources/icons/add_snippet.svg b/src/resources/icons/add_snippet.svg new file mode 100644 index 00000000..c4b273c4 --- /dev/null +++ b/src/resources/icons/add_snippet.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/resources/icons/apply_snippet.svg b/src/resources/icons/apply_snippet.svg new file mode 100644 index 00000000..1865c8bd --- /dev/null +++ b/src/resources/icons/apply_snippet.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/src/resources/icons/delete_snippet.svg b/src/resources/icons/delete_snippet.svg new file mode 100644 index 00000000..1631e74d --- /dev/null +++ b/src/resources/icons/delete_snippet.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/resources/icons/locate_snippet.svg b/src/resources/icons/locate_snippet.svg new file mode 100644 index 00000000..19545aa6 --- /dev/null +++ b/src/resources/icons/locate_snippet.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/resources/icons/snippet_info.svg b/src/resources/icons/snippet_info.svg new file mode 100644 index 00000000..6a72ba5f --- /dev/null +++ b/src/resources/icons/snippet_info.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/resources/icons/snippets.svg b/src/resources/icons/snippets.svg new file mode 100644 index 00000000..05bab769 --- /dev/null +++ b/src/resources/icons/snippets.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/src/src.pro b/src/src.pro index 5ac6e7c0..c8eac51b 100644 --- a/src/src.pro +++ b/src/src.pro @@ -57,6 +57,7 @@ SOURCES += main.cpp\ dialog/vselectdialog.cpp \ vcaptain.cpp \ vopenedlistmenu.cpp \ + vnavigationmode.cpp \ vorphanfile.cpp \ vcodeblockhighlighthelper.cpp \ vwebview.cpp \ @@ -91,7 +92,11 @@ SOURCES += main.cpp\ vpreviewmanager.cpp \ vimageresourcemanager2.cpp \ vtextdocumentlayout.cpp \ - vtextedit.cpp + vtextedit.cpp \ + vsnippetlist.cpp \ + vsnippet.cpp \ + dialog/veditsnippetdialog.cpp \ + utils/vimnavigationforwidget.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -170,7 +175,11 @@ HEADERS += vmainwindow.h \ vpreviewmanager.h \ vimageresourcemanager2.h \ vtextdocumentlayout.h \ - vtextedit.h + vtextedit.h \ + vsnippetlist.h \ + vsnippet.h \ + dialog/veditsnippetdialog.h \ + utils/vimnavigationforwidget.h RESOURCES += \ vnote.qrc \ diff --git a/src/utils/vimnavigationforwidget.cpp b/src/utils/vimnavigationforwidget.cpp new file mode 100644 index 00000000..34df417e --- /dev/null +++ b/src/utils/vimnavigationforwidget.cpp @@ -0,0 +1,69 @@ +#include "vimnavigationforwidget.h" + +#include +#include +#include +#include + +#include "vutils.h" + +VimNavigationForWidget::VimNavigationForWidget() +{ +} + +bool VimNavigationForWidget::injectKeyPressEventForVim(QWidget *p_widget, + QKeyEvent *p_event, + QWidget *p_escWidget) +{ + Q_ASSERT(p_widget); + + bool ret = false; + int key = p_event->key(); + int modifiers = p_event->modifiers(); + if (!p_escWidget) { + p_escWidget = p_widget; + } + + switch (key) { + case Qt::Key_BracketLeft: + { + if (VUtils::isControlModifierForVim(modifiers)) { + QKeyEvent *escEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Escape, + Qt::NoModifier); + QCoreApplication::postEvent(p_escWidget, escEvent); + ret = true; + } + + break; + } + + case Qt::Key_J: + { + if (VUtils::isControlModifierForVim(modifiers)) { + QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, + Qt::NoModifier); + QCoreApplication::postEvent(p_widget, downEvent); + ret = true; + } + + break; + } + + case Qt::Key_K: + { + if (VUtils::isControlModifierForVim(modifiers)) { + QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, + Qt::NoModifier); + QCoreApplication::postEvent(p_widget, upEvent); + ret = true; + } + + break; + } + + default: + break; + } + + return ret; +} diff --git a/src/utils/vimnavigationforwidget.h b/src/utils/vimnavigationforwidget.h new file mode 100644 index 00000000..86028e60 --- /dev/null +++ b/src/utils/vimnavigationforwidget.h @@ -0,0 +1,24 @@ +#ifndef VIMNAVIGATIONFORWIDGET_H +#define VIMNAVIGATIONFORWIDGET_H + +class QWidget; +class QKeyEvent; + + +// Provide simple Vim mode navigation for widgets. +class VimNavigationForWidget +{ +public: + // Try to handle @p_event and inject proper event instead if it triggers + // Vim operation. + // Return true if @p_event is handled properly. + // @p_escWidget: the widget to accept the ESC event. + static bool injectKeyPressEventForVim(QWidget *p_widget, + QKeyEvent *p_event, + QWidget *p_escWidget = nullptr); + +private: + VimNavigationForWidget(); +}; + +#endif // VIMNAVIGATIONFORWIDGET_H diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index bfce455f..ee0be391 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -61,7 +61,7 @@ QString VUtils::readFileFromDisk(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qWarning() << "fail to read file" << filePath; + qWarning() << "fail to open file" << filePath << "to read"; return QString(); } QString fileText(file.readAll()); @@ -84,6 +84,34 @@ bool VUtils::writeFileToDisk(const QString &filePath, const QString &text) return true; } +bool VUtils::writeJsonToDisk(const QString &p_filePath, const QJsonObject &p_json) +{ + QFile file(p_filePath); + // We use Unix LF for config file. + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "fail to open file" << p_filePath << "to write"; + return false; + } + + QJsonDocument doc(p_json); + if (-1 == file.write(doc.toJson())) { + return false; + } + + return true; +} + +QJsonObject VUtils::readJsonFromDisk(const QString &p_filePath) +{ + QFile file(p_filePath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "fail to open file" << p_filePath << "to read"; + return QJsonObject(); + } + + return QJsonDocument::fromJson(file.readAll()).object(); +} + QRgb VUtils::QRgbFromString(const QString &str) { Q_ASSERT(str.length() == 6); @@ -859,6 +887,12 @@ bool VUtils::deleteFile(const VNotebook *p_notebook, } } +bool VUtils::deleteFile(const QString &p_path) +{ + QFile file(p_path); + return file.remove(); +} + bool VUtils::deleteFile(const VOrphanFile *p_file, const QString &p_path, bool p_skipRecycleBin) @@ -1004,3 +1038,12 @@ QString VUtils::validFilePathToOpen(const QString &p_file) return QString(); } + +bool VUtils::isControlModifierForVim(int p_modifiers) +{ +#if defined(Q_OS_MACOS) || defined(Q_OS_MAC) + return p_modifiers == Qt::MetaModifier; +#else + return p_modifiers == Qt::ControlModifier; +#endif +} diff --git a/src/utils/vutils.h b/src/utils/vutils.h index e21a580f..2edc39f7 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -72,9 +72,16 @@ class VUtils { public: static QString readFileFromDisk(const QString &filePath); + static bool writeFileToDisk(const QString &filePath, const QString &text); + + static bool writeJsonToDisk(const QString &p_filePath, const QJsonObject &p_json); + + static QJsonObject readJsonFromDisk(const QString &p_filePath); + // Transform FFFFFF string to QRgb static QRgb QRgbFromString(const QString &str); + static QString generateImageFileName(const QString &path, const QString &title, const QString &format = "png"); @@ -226,6 +233,9 @@ public: const QString &p_path, bool p_skipRecycleBin = false); + // Delete file specified by @p_path. + static bool deleteFile(const QString &p_path); + static QString displayDateTime(const QDateTime &p_dateTime); // Check if file @p_name exists in @p_dir. @@ -242,6 +252,9 @@ public: // Return empty if it is not valid. static QString validFilePathToOpen(const QString &p_file); + // See if @p_modifiers is Control which is different on macOs and Windows. + static bool isControlModifierForVim(int p_modifiers); + // Regular expression for image link. // ![image title]( http://github.com/tamlok/vnote.jpg "alt \" text" ) // Captured texts (need to be trimmed): diff --git a/src/utils/vvim.cpp b/src/utils/vvim.cpp index 8eea6890..a29e9ea7 100644 --- a/src/utils/vvim.cpp +++ b/src/utils/vvim.cpp @@ -24,16 +24,6 @@ const int VVim::SearchHistory::c_capacity = 50; #define ADDKEY(x, y) case (x): {ch = (y); break;} -// See if @p_modifiers is Control which is different on macOs and Windows. -static bool isControlModifier(int p_modifiers) -{ -#if defined(Q_OS_MACOS) || defined(Q_OS_MAC) - return p_modifiers == Qt::MetaModifier; -#else - return p_modifiers == Qt::ControlModifier; -#endif -} - // Returns NULL QChar if invalid. static QChar keyToChar(int p_key, int p_modifiers) { @@ -41,7 +31,7 @@ static QChar keyToChar(int p_key, int p_modifiers) return QChar('0' + (p_key - Qt::Key_0)); } else if (p_key >= Qt::Key_A && p_key <= Qt::Key_Z) { if (p_modifiers == Qt::ShiftModifier - || isControlModifier(p_modifiers)) { + || VUtils::isControlModifierForVim(p_modifiers)) { return QChar('A' + (p_key - Qt::Key_A)); } else { return QChar('a' + (p_key - Qt::Key_A)); @@ -99,7 +89,7 @@ static QString keyToString(int p_key, int p_modifiers) return QString(); } - if (isControlModifier(p_modifiers)) { + if (VUtils::isControlModifierForVim(p_modifiers)) { return QString("^") + ch; } else { return ch; @@ -473,7 +463,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) // Handle Insert mode key press. if (VimMode::Insert == m_mode) { if (key == Qt::Key_Escape - || (key == Qt::Key_BracketLeft && isControlModifier(modifiers))) { + || (key == Qt::Key_BracketLeft && VUtils::isControlModifierForVim(modifiers))) { // See if we need to cancel auto indent. bool cancelAutoIndent = false; if (p_autoIndentPos && *p_autoIndentPos > -1) { @@ -510,14 +500,14 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) } goto clear_accept; - } else if (key == Qt::Key_R && isControlModifier(modifiers)) { + } else if (key == Qt::Key_R && VUtils::isControlModifierForVim(modifiers)) { // Ctrl+R, insert the content of a register. m_pendingKeys.append(keyInfo); m_registerPending = true; goto accept; } - if (key == Qt::Key_O && isControlModifier(modifiers)) { + if (key == Qt::Key_O && VUtils::isControlModifierForVim(modifiers)) { // Ctrl+O, enter normal mode, execute one command, then return to insert mode. m_insertModeAfterCommand = true; clearSelection(); @@ -823,7 +813,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) m_editor->setTextCursorW(cursor); setMode(VimMode::Insert); - } else if (isControlModifier(modifiers)) { + } else if (VUtils::isControlModifierForVim(modifiers)) { // Ctrl+I, jump to next location. if (!m_tokens.isEmpty() || !checkMode(VimMode::Normal)) { @@ -935,7 +925,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) } break; - } else if (isControlModifier(modifiers)) { + } else if (VUtils::isControlModifierForVim(modifiers)) { // Ctrl+O, jump to previous location. if (!m_tokens.isEmpty() || !checkMode(VimMode::Normal)) { @@ -1037,7 +1027,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) // Should be kept together with Qt::Key_PageUp. case Qt::Key_B: { - if (isControlModifier(modifiers)) { + if (VUtils::isControlModifierForVim(modifiers)) { // Ctrl+B, page up, fall through. modifiers = Qt::NoModifier; } else if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) { @@ -1095,7 +1085,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) tryGetRepeatToken(m_keys, m_tokens); bool toLower = modifiers == Qt::NoModifier; - if (isControlModifier(modifiers)) { + if (VUtils::isControlModifierForVim(modifiers)) { // Ctrl+U, HalfPageUp. if (!m_keys.isEmpty()) { // Not a valid sequence. @@ -1200,7 +1190,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) case Qt::Key_D: { - if (isControlModifier(modifiers)) { + if (VUtils::isControlModifierForVim(modifiers)) { // Ctrl+D, HalfPageDown. tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { @@ -1301,7 +1291,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) // Should be kept together with Qt::Key_Escape. case Qt::Key_BracketLeft: { - if (isControlModifier(modifiers)) { + if (VUtils::isControlModifierForVim(modifiers)) { // fallthrough. } else if (modifiers == Qt::NoModifier) { tryGetRepeatToken(m_keys, m_tokens); @@ -1792,7 +1782,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) break; } - if (isControlModifier(modifiers)) { + if (VUtils::isControlModifierForVim(modifiers)) { // Redo. tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty() || hasActionToken()) { diff --git a/src/vattachmentlist.cpp b/src/vattachmentlist.cpp index 8c44044b..a2936f4d 100644 --- a/src/vattachmentlist.cpp +++ b/src/vattachmentlist.cpp @@ -8,6 +8,7 @@ #include "vmainwindow.h" #include "dialog/vconfirmdeletiondialog.h" #include "dialog/vsortdialog.h" +#include "utils/vimnavigationforwidget.h" extern VConfigManager *g_config; extern VMainWindow *g_mainWin; @@ -462,47 +463,10 @@ void VAttachmentList::handleListItemCommitData(QWidget *p_itemEdit) void VAttachmentList::keyPressEvent(QKeyEvent *p_event) { - int key = p_event->key(); - int modifiers = p_event->modifiers(); - switch (key) { - case Qt::Key_BracketLeft: - { - if (modifiers == Qt::ControlModifier) { - QKeyEvent *escEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Escape, - Qt::NoModifier); - QCoreApplication::postEvent(this, escEvent); - return; - } - - break; - } - - case Qt::Key_J: - { - if (modifiers == Qt::ControlModifier) { - QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, - Qt::NoModifier); - QCoreApplication::postEvent(m_attachmentList, downEvent); - return; - } - - break; - } - - case Qt::Key_K: - { - if (modifiers == Qt::ControlModifier) { - QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, - Qt::NoModifier); - QCoreApplication::postEvent(m_attachmentList, upEvent); - return; - } - - break; - } - - default: - break; + if (VimNavigationForWidget::injectKeyPressEventForVim(m_attachmentList, + p_event, + this)) { + return; } QWidget::keyPressEvent(p_event); diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index e4ae3047..44d817c6 100644 --- a/src/vconfigmanager.cpp +++ b/src/vconfigmanager.cpp @@ -28,12 +28,16 @@ const QString VConfigManager::c_defaultConfigFile = QString("vnote.ini"); const QString VConfigManager::c_sessionConfigFile = QString("session.ini"); +const QString VConfigManager::c_snippetConfigFile = QString("snippet.json"); + const QString VConfigManager::c_styleConfigFolder = QString("styles"); const QString VConfigManager::c_codeBlockStyleConfigFolder = QString("codeblock_styles"); const QString VConfigManager::c_templateConfigFolder = QString("templates"); +const QString VConfigManager::c_snippetConfigFolder = QString("snippets"); + const QString VConfigManager::c_defaultCssFile = QString(":/resources/styles/default.css"); const QString VConfigManager::c_defaultCodeBlockCssFile = QString(":/utils/highlightjs/styles/vnote.css"); @@ -483,6 +487,7 @@ bool VConfigManager::writeDirectoryConfig(const QString &path, const QJsonObject QString configFile = fetchDirConfigFilePath(path); QFile config(configFile); + // We use Unix LF for config file. if (!config.open(QIODevice::WriteOnly)) { qWarning() << "fail to open directory configuration file for write:" << configFile; @@ -734,17 +739,32 @@ QString VConfigManager::getConfigFilePath() const QString VConfigManager::getStyleConfigFolder() const { - return getConfigFolder() + QDir::separator() + c_styleConfigFolder; + static QString path = QDir(getConfigFolder()).filePath(c_styleConfigFolder); + return path; } QString VConfigManager::getCodeBlockStyleConfigFolder() const { - return getStyleConfigFolder() + QDir::separator() + c_codeBlockStyleConfigFolder; + static QString path = QDir(getStyleConfigFolder()).filePath(c_codeBlockStyleConfigFolder); + return path; } QString VConfigManager::getTemplateConfigFolder() const { - return getConfigFolder() + QDir::separator() + c_templateConfigFolder; + static QString path = QDir(getConfigFolder()).filePath(c_templateConfigFolder); + return path; +} + +QString VConfigManager::getSnippetConfigFolder() const +{ + static QString path = QDir(getConfigFolder()).filePath(c_snippetConfigFolder); + return path; +} + +QString VConfigManager::getSnippetConfigFilePath() const +{ + static QString path = QDir(getSnippetConfigFolder()).filePath(c_snippetConfigFile); + return path; } QVector VConfigManager::getCssStyles() const diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 15bdcab5..faf1bee5 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -352,6 +352,11 @@ public: // Get the folder c_templateConfigFolder in the config folder. QString getTemplateConfigFolder() const; + // Get the folder c_snippetConfigFolder in the config folder. + QString getSnippetConfigFolder() const; + + QString getSnippetConfigFilePath() const; + // Read all available css files in c_styleConfigFolder. QVector getCssStyles() const; @@ -714,6 +719,9 @@ private: // The name of the config file for session information. static const QString c_sessionConfigFile; + // The name of the config file for snippets folder. + static const QString c_snippetConfigFile; + // QSettings for the user configuration QSettings *userSettings; @@ -733,6 +741,9 @@ private: // The folder name of template files. static const QString c_templateConfigFolder; + // The folder name of snippet files. + static const QString c_snippetConfigFolder; + // Default CSS file in resource system. static const QString c_defaultCssFile; diff --git a/src/vconstants.h b/src/vconstants.h index 972b6b1b..7ab37955 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -50,6 +50,18 @@ namespace DirConfig static const QString c_modifiedTime = "modified_time"; } +// Snippet Cofnig file items. +namespace SnippetConfig +{ + static const QString c_version = "version"; + static const QString c_snippets = "snippets"; + static const QString c_name = "name"; + static const QString c_type = "type"; + static const QString c_cursorMark = "cursor_mark"; + static const QString c_selectionMark = "selection_mark"; + static const QString c_shortcut = "shortcut"; +} + static const QString c_emptyHeaderName = "[EMPTY]"; enum class TextDecoration diff --git a/src/vdirectorytree.cpp b/src/vdirectorytree.cpp index 41c78cf1..63b30815 100644 --- a/src/vdirectorytree.cpp +++ b/src/vdirectorytree.cpp @@ -10,6 +10,7 @@ #include "vconfigmanager.h" #include "vmainwindow.h" #include "dialog/vsortdialog.h" +#include "utils/vimnavigationforwidget.h" extern VMainWindow *g_mainWin; @@ -910,6 +911,10 @@ void VDirectoryTree::mousePressEvent(QMouseEvent *event) void VDirectoryTree::keyPressEvent(QKeyEvent *event) { + if (VimNavigationForWidget::injectKeyPressEventForVim(this, event)) { + return; + } + int key = event->key(); int modifiers = event->modifiers(); @@ -924,32 +929,6 @@ void VDirectoryTree::keyPressEvent(QKeyEvent *event) break; } - case Qt::Key_J: - { - if (modifiers == Qt::ControlModifier) { - event->accept(); - QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, - Qt::NoModifier); - QCoreApplication::postEvent(this, downEvent); - return; - } - - break; - } - - case Qt::Key_K: - { - if (modifiers == Qt::ControlModifier) { - event->accept(); - QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, - Qt::NoModifier); - QCoreApplication::postEvent(this, upEvent); - return; - } - - break; - } - case Qt::Key_Asterisk: { if (modifiers == Qt::ShiftModifier) { @@ -1089,110 +1068,18 @@ void VDirectoryTree::expandSubTree(QTreeWidgetItem *p_item) } } -void VDirectoryTree::registerNavigation(QChar p_majorKey) -{ - m_majorKey = p_majorKey; - V_ASSERT(m_keyMap.empty()); - V_ASSERT(m_naviLabels.empty()); -} - void VDirectoryTree::showNavigation() { - // Clean up. - m_keyMap.clear(); - for (auto label : m_naviLabels) { - delete label; - } - m_naviLabels.clear(); - - if (!isVisible()) { - return; - } - - // Generate labels for visible items. - auto items = getVisibleItems(); - for (int i = 0; i < 26 && i < items.size(); ++i) { - QChar key('a' + i); - m_keyMap[key] = items[i]; - - QString str = QString(m_majorKey) + key; - QLabel *label = new QLabel(str, this); - label->setStyleSheet(g_vnote->getNavigationLabelStyle(str)); - label->move(visualItemRect(items[i]).topLeft()); - label->show(); - m_naviLabels.append(label); - } -} - -void VDirectoryTree::hideNavigation() -{ - m_keyMap.clear(); - for (auto label : m_naviLabels) { - delete label; - } - m_naviLabels.clear(); + VNavigationMode::showNavigation(this); } bool VDirectoryTree::handleKeyNavigation(int p_key, bool &p_succeed) { static bool secondKey = false; - bool ret = false; - p_succeed = false; - QChar keyChar = VUtils::keyToChar(p_key); - if (secondKey && !keyChar.isNull()) { - secondKey = false; - p_succeed = true; - ret = true; - auto it = m_keyMap.find(keyChar); - if (it != m_keyMap.end()) { - setCurrentItem(it.value()); - setFocus(); - } - } else if (keyChar == m_majorKey) { - // Major key pressed. - // Need second key if m_keyMap is not empty. - if (m_keyMap.isEmpty()) { - p_succeed = true; - } else { - secondKey = true; - } - ret = true; - } - return ret; -} - -QList VDirectoryTree::getVisibleItems() const -{ - QList items; - for (int i = 0; i < topLevelItemCount(); ++i) { - QTreeWidgetItem *item = topLevelItem(i); - if (!item->isHidden()) { - items.append(item); - if (item->isExpanded()) { - items.append(getVisibleChildItems(item)); - } - } - } - - return items; -} - -QList VDirectoryTree::getVisibleChildItems(const QTreeWidgetItem *p_item) const -{ - QList items; - if (p_item && !p_item->isHidden() && p_item->isExpanded()) { - for (int i = 0; i < p_item->childCount(); ++i) { - QTreeWidgetItem *child = p_item->child(i); - if (!child->isHidden()) { - items.append(child); - if (child->isExpanded()) { - items.append(getVisibleChildItems(child)); - } - } - } - } - - return items; + return VNavigationMode::handleKeyNavigation(this, + secondKey, + p_key, + p_succeed); } int VDirectoryTree::getNewMagic() diff --git a/src/vdirectorytree.h b/src/vdirectorytree.h index 16740a8f..eb2df598 100644 --- a/src/vdirectorytree.h +++ b/src/vdirectorytree.h @@ -29,9 +29,7 @@ public: const VNotebook *currentNotebook() const; // Implementations for VNavigationMode. - void registerNavigation(QChar p_majorKey) Q_DECL_OVERRIDE; void showNavigation() Q_DECL_OVERRIDE; - void hideNavigation() Q_DECL_OVERRIDE; bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE; signals: @@ -134,10 +132,6 @@ private: // Expand the currently-built subtree of @p_item according to VDirectory.isExpanded(). void expandSubTree(QTreeWidgetItem *p_item); - QList getVisibleItems() const; - - QList getVisibleChildItems(const QTreeWidgetItem *p_item) const; - // We use a map to save and restore current directory of each notebook. // Try to restore current directory after changing notebook. // Return false if no cache item found for current notebook. @@ -180,11 +174,6 @@ private: // Reload content from disk. QAction *m_reloadAct; - // Navigation Mode. - // Map second key to QTreeWidgetItem. - QMap m_keyMap; - QVector m_naviLabels; - static const QString c_infoShortcutSequence; static const QString c_copyShortcutSequence; static const QString c_cutShortcutSequence; diff --git a/src/veditarea.cpp b/src/veditarea.cpp index 90784622..7f5e2267 100644 --- a/src/veditarea.cpp +++ b/src/veditarea.cpp @@ -756,7 +756,8 @@ bool VEditArea::handleKeyNavigation(int p_key, bool &p_succeed) ret = true; auto it = m_keyMap.find(keyChar); if (it != m_keyMap.end()) { - setCurrentWindow(splitter->indexOf(it.value()), true); + setCurrentWindow(splitter->indexOf(static_cast(it.value())), + true); } } else if (keyChar == m_majorKey) { // Major key pressed. diff --git a/src/veditarea.h b/src/veditarea.h index e434dca9..4c621e4b 100644 --- a/src/veditarea.h +++ b/src/veditarea.h @@ -205,11 +205,6 @@ private: // Last closed files stack. QStack m_lastClosedFiles; - - // Navigation Mode. - // Map second key to VEditWindow. - QMap m_keyMap; - QVector m_naviLabels; }; inline VEditWindow* VEditArea::getWindow(int windowIndex) const diff --git a/src/veditor.cpp b/src/veditor.cpp index 2137aee9..c3a73da4 100644 --- a/src/veditor.cpp +++ b/src/veditor.cpp @@ -9,6 +9,7 @@ #include "veditoperations.h" #include "dialog/vinsertlinkdialog.h" #include "utils/vmetawordmanager.h" +#include "utils/vvim.h" extern VConfigManager *g_config; @@ -915,3 +916,10 @@ void VEditor::updateConfig() { updateEditConfig(); } + +void VEditor::setVimMode(VimMode p_mode) +{ + if (m_editOps) { + m_editOps->setVimMode(p_mode); + } +} diff --git a/src/veditor.h b/src/veditor.h index 4c9ab8ab..63a2e892 100644 --- a/src/veditor.h +++ b/src/veditor.h @@ -16,6 +16,7 @@ class VEditOperations; class QTimer; class QLabel; class VVim; +enum class VimMode; enum class SelectionId { @@ -136,6 +137,8 @@ public: // Update config according to global configurations. virtual void updateConfig(); + void setVimMode(VimMode p_mode); + // Wrapper functions for QPlainTextEdit/QTextEdit. // Ends with W to distinguish it from the original interfaces. public: diff --git a/src/vedittab.cpp b/src/vedittab.cpp index db8c4400..04dc8387 100644 --- a/src/vedittab.cpp +++ b/src/vedittab.cpp @@ -124,3 +124,8 @@ bool VEditTab::tabHasFocus() const void VEditTab::insertLink() { } + +void VEditTab::applySnippet(const VSnippet *p_snippet) +{ + Q_UNUSED(p_snippet); +} diff --git a/src/vedittab.h b/src/vedittab.h index 23110fa2..d04742fb 100644 --- a/src/vedittab.h +++ b/src/vedittab.h @@ -10,6 +10,7 @@ #include "vedittabinfo.h" class VEditArea; +class VSnippet; // VEditTab is the base class of an edit tab inside VEditWindow. class VEditTab : public QWidget @@ -91,6 +92,9 @@ public: // Called by evaluateMagicWordsByCaptain() to evaluate the magic words. virtual void evaluateMagicWords(); + // Insert snippet @p_snippet. + virtual void applySnippet(const VSnippet *p_snippet); + public slots: // Enter edit mode virtual void editFile() = 0; diff --git a/src/vfilelist.cpp b/src/vfilelist.cpp index 37fb66b6..d1a1a078 100644 --- a/src/vfilelist.cpp +++ b/src/vfilelist.cpp @@ -15,6 +15,7 @@ #include "dialog/vconfirmdeletiondialog.h" #include "dialog/vsortdialog.h" #include "vmainwindow.h" +#include "utils/vimnavigationforwidget.h" extern VConfigManager *g_config; extern VNote *g_vnote; @@ -825,50 +826,21 @@ void VFileList::pasteFiles(VDirectory *p_destDir, getNewMagic(); } -void VFileList::keyPressEvent(QKeyEvent *event) +void VFileList::keyPressEvent(QKeyEvent *p_event) { - int key = event->key(); - int modifiers = event->modifiers(); - switch (key) { - case Qt::Key_Return: - { + if (VimNavigationForWidget::injectKeyPressEventForVim(fileList, + p_event)) { + return; + } + + if (p_event->key() == Qt::Key_Return) { QListWidgetItem *item = fileList->currentItem(); if (item) { handleItemClicked(item); } - - break; } - - case Qt::Key_J: - { - if (modifiers == Qt::ControlModifier) { - event->accept(); - QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, - Qt::NoModifier); - QCoreApplication::postEvent(fileList, downEvent); - return; - } - break; - } - - case Qt::Key_K: - { - if (modifiers == Qt::ControlModifier) { - event->accept(); - QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, - Qt::NoModifier); - QCoreApplication::postEvent(fileList, upEvent); - return; - } - break; - } - - default: - break; - } - QWidget::keyPressEvent(event); + QWidget::keyPressEvent(p_event); } void VFileList::focusInEvent(QFocusEvent * /* p_event */) @@ -893,91 +865,15 @@ bool VFileList::locateFile(const VNoteFile *p_file) return false; } -void VFileList::registerNavigation(QChar p_majorKey) -{ - m_majorKey = p_majorKey; - V_ASSERT(m_keyMap.empty()); - V_ASSERT(m_naviLabels.empty()); -} - void VFileList::showNavigation() { - // Clean up. - m_keyMap.clear(); - for (auto label : m_naviLabels) { - delete label; - } - m_naviLabels.clear(); - - if (!isVisible()) { - return; - } - - // Generate labels for visible items. - auto items = getVisibleItems(); - int itemWidth = rect().width(); - for (int i = 0; i < 26 && i < items.size(); ++i) { - QChar key('a' + i); - m_keyMap[key] = items[i]; - - QString str = QString(m_majorKey) + key; - QLabel *label = new QLabel(str, this); - label->setStyleSheet(g_vnote->getNavigationLabelStyle(str)); - label->show(); - QRect rect = fileList->visualItemRect(items[i]); - // Display the label at the end to show the file name. - label->move(rect.x() + itemWidth - label->width(), rect.y()); - m_naviLabels.append(label); - } -} - -void VFileList::hideNavigation() -{ - m_keyMap.clear(); - for (auto label : m_naviLabels) { - delete label; - } - m_naviLabels.clear(); + VNavigationMode::showNavigation(fileList); } bool VFileList::handleKeyNavigation(int p_key, bool &p_succeed) { static bool secondKey = false; - bool ret = false; - p_succeed = false; - QChar keyChar = VUtils::keyToChar(p_key); - if (secondKey && !keyChar.isNull()) { - secondKey = false; - p_succeed = true; - ret = true; - auto it = m_keyMap.find(keyChar); - if (it != m_keyMap.end()) { - fileList->setCurrentItem(it.value(), QItemSelectionModel::ClearAndSelect); - fileList->setFocus(); - } - } else if (keyChar == m_majorKey) { - // Major key pressed. - // Need second key if m_keyMap is not empty. - if (m_keyMap.isEmpty()) { - p_succeed = true; - } else { - secondKey = true; - } - ret = true; - } - return ret; -} - -QList VFileList::getVisibleItems() const -{ - QList items; - for (int i = 0; i < fileList->count(); ++i) { - QListWidgetItem *item = fileList->item(i); - if (!item->isHidden()) { - items.append(item); - } - } - return items; + return VNavigationMode::handleKeyNavigation(fileList, secondKey, p_key, p_succeed); } int VFileList::getNewMagic() diff --git a/src/vfilelist.h b/src/vfilelist.h index 6fa0258f..56b43bc8 100644 --- a/src/vfilelist.h +++ b/src/vfilelist.h @@ -49,9 +49,7 @@ public: QWidget *getContentWidget() const; // Implementations for VNavigationMode. - void registerNavigation(QChar p_majorKey) Q_DECL_OVERRIDE; void showNavigation() Q_DECL_OVERRIDE; - void hideNavigation() Q_DECL_OVERRIDE; bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE; public slots: @@ -104,7 +102,8 @@ private slots: void sortItems(); protected: - void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; + void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; + void focusInEvent(QFocusEvent *p_event) Q_DECL_OVERRIDE; private: @@ -140,8 +139,6 @@ private: inline QPointer getVFile(QListWidgetItem *p_item) const; - QList getVisibleItems() const; - // Fill the info of @p_item according to @p_file. void fillItem(QListWidgetItem *p_item, const VNoteFile *p_file); @@ -174,11 +171,6 @@ private: QAction *m_openLocationAct; QAction *m_sortAct; - // Navigation Mode. - // Map second key to QListWidgetItem. - QMap m_keyMap; - QVector m_naviLabels; - static const QString c_infoShortcutSequence; static const QString c_copyShortcutSequence; static const QString c_cutShortcutSequence; diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index 8df3bbe1..b9aee1eb 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -30,6 +30,7 @@ #include "vbuttonwithwidget.h" #include "vattachmentlist.h" #include "vfilesessioninfo.h" +#include "vsnippetlist.h" VMainWindow *g_mainWin; @@ -111,6 +112,7 @@ void VMainWindow::registerCaptainAndNavigationTargets() m_captain->registerNavigationTarget(m_fileList); m_captain->registerNavigationTarget(editArea); m_captain->registerNavigationTarget(outline); + m_captain->registerNavigationTarget(m_snippetList); // Register Captain mode targets. m_captain->registerCaptainTarget(tr("AttachmentList"), @@ -1185,7 +1187,6 @@ void VMainWindow::initDockWindows() toolDock = new QDockWidget(tr("Tools"), this); toolDock->setObjectName("ToolsDock"); toolDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); - toolBox = new QToolBox(this); // Outline tree. outline = new VOutline(this); @@ -1196,8 +1197,18 @@ void VMainWindow::initDockWindows() connect(outline, &VOutline::outlineItemActivated, editArea, &VEditArea::scrollToHeader); - toolBox->addItem(outline, QIcon(":/resources/icons/outline.svg"), tr("Outline")); - toolDock->setWidget(toolBox); + // Snippets. + m_snippetList = new VSnippetList(this); + + m_toolBox = new QToolBox(this); + m_toolBox->addItem(outline, + QIcon(":/resources/icons/outline.svg"), + tr("Outline")); + m_toolBox->addItem(m_snippetList, + QIcon(":/resources/icons/snippets.svg"), + tr("Snippets")); + + toolDock->setWidget(m_toolBox); addDockWidget(Qt::RightDockWidgetArea, toolDock); QAction *toggleAct = toolDock->toggleViewAction(); diff --git a/src/vmainwindow.h b/src/vmainwindow.h index 3badc996..e1428282 100644 --- a/src/vmainwindow.h +++ b/src/vmainwindow.h @@ -37,6 +37,7 @@ class QSystemTrayIcon; class QShortcut; class VButtonWithWidget; class VAttachmentList; +class VSnippetList; enum class PanelViewState { @@ -87,6 +88,8 @@ public: VFile *getCurrentFile() const; + VEditTab *getCurrentTab() const; + signals: // Emit when editor related configurations were changed by user. void editorConfigUpdated(); @@ -294,8 +297,15 @@ private: VEditArea *editArea; QDockWidget *toolDock; - QToolBox *toolBox; + + // Tool box in the dock widget. + QToolBox *m_toolBox; + VOutline *outline; + + // View and manage snippets. + VSnippetList *m_snippetList; + VAvatar *m_avatar; VFindReplaceDialog *m_findReplaceDialog; VVimIndicator *m_vimIndicator; @@ -396,4 +406,9 @@ inline VFile *VMainWindow::getCurrentFile() const return m_curFile; } +inline VEditTab *VMainWindow::getCurrentTab() const +{ + return m_curTab; +} + #endif // VMAINWINDOW_H diff --git a/src/vmdeditor.h b/src/vmdeditor.h index 12f10f17..c914a098 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -10,7 +10,6 @@ #include "veditor.h" #include "vconfigmanager.h" #include "vtableofcontent.h" -#include "veditoperations.h" #include "vconfigmanager.h" #include "utils/vutils.h" diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index e111890d..1310e350 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -18,6 +18,7 @@ #include "vwebview.h" #include "vmdeditor.h" #include "vmainwindow.h" +#include "vsnippet.h" extern VMainWindow *g_mainWin; @@ -724,3 +725,19 @@ void VMdTab::evaluateMagicWords() getEditor()->evaluateMagicWords(); } } + +void VMdTab::applySnippet(const VSnippet *p_snippet) +{ + if (isEditMode() + && m_file->isModifiable() + && p_snippet->getType() == VSnippet::Type::PlainText) { + Q_ASSERT(m_editor); + QTextCursor cursor = m_editor->textCursor(); + bool changed = p_snippet->apply(cursor); + if (changed) { + m_editor->setTextCursor(cursor); + + m_editor->setVimMode(VimMode::Insert); + } + } +} diff --git a/src/vmdtab.h b/src/vmdtab.h index bc674905..922edc1e 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -75,6 +75,8 @@ public: // Evaluate magic words. void evaluateMagicWords() Q_DECL_OVERRIDE; + void applySnippet(const VSnippet *p_snippet) Q_DECL_OVERRIDE; + public slots: // Enter edit mode. void editFile() Q_DECL_OVERRIDE; diff --git a/src/vnavigationmode.cpp b/src/vnavigationmode.cpp new file mode 100644 index 00000000..cafd1601 --- /dev/null +++ b/src/vnavigationmode.cpp @@ -0,0 +1,202 @@ +#include "vnavigationmode.h" + +#include +#include +#include +#include + +#include "vnote.h" +#include "utils/vutils.h" + +extern VNote *g_vnote; + +VNavigationMode::VNavigationMode() +{ +} + +VNavigationMode::~VNavigationMode() +{ +} + +void VNavigationMode::registerNavigation(QChar p_majorKey) +{ + m_majorKey = p_majorKey; + Q_ASSERT(m_keyMap.empty()); + Q_ASSERT(m_naviLabels.empty()); +} + +void VNavigationMode::hideNavigation() +{ + clearNavigation(); +} + +void VNavigationMode::showNavigation(QListWidget *p_widget) +{ + clearNavigation(); + + if (!p_widget->isVisible()) { + return; + } + + // Generate labels for visible items. + auto items = getVisibleItems(p_widget); + for (int i = 0; i < 26 && i < items.size(); ++i) { + QChar key('a' + i); + m_keyMap[key] = items[i]; + + QString str = QString(m_majorKey) + key; + QLabel *label = new QLabel(str, p_widget); + label->setStyleSheet(g_vnote->getNavigationLabelStyle(str)); + label->show(); + QRect rect = p_widget->visualItemRect(items[i]); + // Display the label at the end to show the file name. + label->move(rect.x() + p_widget->rect().width() - label->width() - 2, + rect.y()); + m_naviLabels.append(label); + } +} + +QList VNavigationMode::getVisibleItems(const QListWidget *p_widget) const +{ + QList items; + for (int i = 0; i < p_widget->count(); ++i) { + QListWidgetItem *item = p_widget->item(i); + if (!item->isHidden()) { + items.append(item); + } + } + + return items; +} + +static QList getVisibleChildItems(const QTreeWidgetItem *p_item) +{ + QList items; + if (p_item && !p_item->isHidden() && p_item->isExpanded()) { + for (int i = 0; i < p_item->childCount(); ++i) { + QTreeWidgetItem *child = p_item->child(i); + if (!child->isHidden()) { + items.append(child); + if (child->isExpanded()) { + items.append(getVisibleChildItems(child)); + } + } + } + } + + return items; +} + +QList VNavigationMode::getVisibleItems(const QTreeWidget *p_widget) const +{ + QList items; + for (int i = 0; i < p_widget->topLevelItemCount(); ++i) { + QTreeWidgetItem *item = p_widget->topLevelItem(i); + if (!item->isHidden()) { + items.append(item); + if (item->isExpanded()) { + items.append(getVisibleChildItems(item)); + } + } + } + + return items; +} + +bool VNavigationMode::handleKeyNavigation(QListWidget *p_widget, + bool &p_secondKey, + int p_key, + bool &p_succeed) +{ + bool ret = false; + p_succeed = false; + QChar keyChar = VUtils::keyToChar(p_key); + if (p_secondKey && !keyChar.isNull()) { + p_secondKey = false; + p_succeed = true; + ret = true; + auto it = m_keyMap.find(keyChar); + if (it != m_keyMap.end()) { + p_widget->setCurrentItem(static_cast(it.value()), + QItemSelectionModel::ClearAndSelect); + p_widget->setFocus(); + } + } else if (keyChar == m_majorKey) { + // Major key pressed. + // Need second key if m_keyMap is not empty. + if (m_keyMap.isEmpty()) { + p_succeed = true; + } else { + p_secondKey = true; + } + + ret = true; + } + + return ret; +} + +void VNavigationMode::showNavigation(QTreeWidget *p_widget) +{ + clearNavigation(); + + if (!p_widget->isVisible()) { + return; + } + + // Generate labels for visible items. + auto items = getVisibleItems(p_widget); + for (int i = 0; i < 26 && i < items.size(); ++i) { + QChar key('a' + i); + m_keyMap[key] = items[i]; + + QString str = QString(m_majorKey) + key; + QLabel *label = new QLabel(str, p_widget); + label->setStyleSheet(g_vnote->getNavigationLabelStyle(str)); + label->move(p_widget->visualItemRect(items[i]).topLeft()); + label->show(); + m_naviLabels.append(label); + } +} + +void VNavigationMode::clearNavigation() +{ + m_keyMap.clear(); + for (auto label : m_naviLabels) { + delete label; + } + + m_naviLabels.clear(); +} + +bool VNavigationMode::handleKeyNavigation(QTreeWidget *p_widget, + bool &p_secondKey, + int p_key, + bool &p_succeed) +{ + bool ret = false; + p_succeed = false; + QChar keyChar = VUtils::keyToChar(p_key); + if (p_secondKey && !keyChar.isNull()) { + p_secondKey = false; + p_succeed = true; + ret = true; + auto it = m_keyMap.find(keyChar); + if (it != m_keyMap.end()) { + p_widget->setCurrentItem(static_cast(it.value())); + p_widget->setFocus(); + } + } else if (keyChar == m_majorKey) { + // Major key pressed. + // Need second key if m_keyMap is not empty. + if (m_keyMap.isEmpty()) { + p_succeed = true; + } else { + p_secondKey = true; + } + + ret = true; + } + + return ret; +} diff --git a/src/vnavigationmode.h b/src/vnavigationmode.h index a1af73a3..a1a45376 100644 --- a/src/vnavigationmode.h +++ b/src/vnavigationmode.h @@ -2,23 +2,63 @@ #define VNAVIGATIONMODE_H #include +#include +#include +#include + +class QLabel; +class QListWidget; +class QListWidgetItem; +class QTreeWidget; +class QTreeWidgetItem; + // Interface class for Navigation Mode in Captain Mode. class VNavigationMode { public: - VNavigationMode() {}; - virtual ~VNavigationMode() {}; + VNavigationMode(); + + virtual ~VNavigationMode(); + + virtual void registerNavigation(QChar p_majorKey); - virtual void registerNavigation(QChar p_majorKey) = 0; virtual void showNavigation() = 0; - virtual void hideNavigation() = 0; + + virtual void hideNavigation(); + // Return true if this object could consume p_key. // p_succeed indicates whether the keys hit a target successfully. virtual bool handleKeyNavigation(int p_key, bool &p_succeed) = 0; protected: + void clearNavigation(); + + void showNavigation(QListWidget *p_widget); + + void showNavigation(QTreeWidget *p_widget); + + bool handleKeyNavigation(QListWidget *p_widget, + bool &p_secondKey, + int p_key, + bool &p_succeed); + + bool handleKeyNavigation(QTreeWidget *p_widget, + bool &p_secondKey, + int p_key, + bool &p_succeed); + QChar m_majorKey; + + // Map second key to item. + QMap m_keyMap; + + QVector m_naviLabels; + +private: + QList getVisibleItems(const QListWidget *p_widget) const; + + QList getVisibleItems(const QTreeWidget *p_widget) const; }; #endif // VNAVIGATIONMODE_H diff --git a/src/vnote.qrc b/src/vnote.qrc index 155fbef1..c582a282 100644 --- a/src/vnote.qrc +++ b/src/vnote.qrc @@ -135,5 +135,11 @@ resources/icons/link.svg resources/icons/code_block.svg resources/icons/manage_template.svg + resources/icons/snippets.svg + resources/icons/add_snippet.svg + resources/icons/locate_snippet.svg + resources/icons/delete_snippet.svg + resources/icons/snippet_info.svg + resources/icons/apply_snippet.svg diff --git a/src/vnotebookselector.cpp b/src/vnotebookselector.cpp index 0a0696e4..31800afb 100644 --- a/src/vnotebookselector.cpp +++ b/src/vnotebookselector.cpp @@ -21,6 +21,7 @@ #include "veditarea.h" #include "vnofocusitemdelegate.h" #include "vmainwindow.h" +#include "utils/vimnavigationforwidget.h" extern VConfigManager *g_config; @@ -590,45 +591,9 @@ bool VNotebookSelector::handleKeyNavigation(int p_key, bool &p_succeed) bool VNotebookSelector::handlePopupKeyPress(QKeyEvent *p_event) { - int key = p_event->key(); - int modifiers = p_event->modifiers(); - switch (key) { - case Qt::Key_BracketLeft: - { - if (modifiers == Qt::ControlModifier) { - p_event->accept(); - hidePopup(); - return true; - } - break; - } - - case Qt::Key_J: - { - if (modifiers == Qt::ControlModifier) { - p_event->accept(); - QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, - Qt::NoModifier); - QCoreApplication::postEvent(m_listWidget, downEvent); - return true; - } - break; - } - - case Qt::Key_K: - { - if (modifiers == Qt::ControlModifier) { - p_event->accept(); - QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, - Qt::NoModifier); - QCoreApplication::postEvent(m_listWidget, upEvent); - return true; - } - break; - } - - default: - break; + if (VimNavigationForWidget::injectKeyPressEventForVim(m_listWidget, + p_event)) { + return true; } return false; diff --git a/src/vopenedlistmenu.cpp b/src/vopenedlistmenu.cpp index 4daf55eb..df8eb38c 100644 --- a/src/vopenedlistmenu.cpp +++ b/src/vopenedlistmenu.cpp @@ -194,6 +194,7 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event) hide(); return; } + break; } @@ -201,7 +202,7 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event) { m_cmdTimer->stop(); m_cmdNum = 0; - if (modifiers == Qt::ControlModifier) { + if (VUtils::isControlModifierForVim(modifiers)) { QList acts = actions(); if (acts.size() == 0) { return; @@ -230,6 +231,7 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event) setActiveAction(act); return; } + break; } @@ -237,11 +239,12 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event) { m_cmdTimer->stop(); m_cmdNum = 0; - if (modifiers == Qt::ControlModifier) { + if (VUtils::isControlModifierForVim(modifiers)) { QList acts = actions(); if (acts.size() == 0) { return; } + int idx = acts.size() - 1; QAction *act = activeAction(); if (act) { @@ -266,6 +269,7 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event) setActiveAction(act); return; } + break; } @@ -274,6 +278,7 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event) m_cmdNum = 0; break; } + QMenu::keyPressEvent(p_event); } diff --git a/src/voutline.cpp b/src/voutline.cpp index c4a6c299..65e5f879 100644 --- a/src/voutline.cpp +++ b/src/voutline.cpp @@ -221,108 +221,18 @@ void VOutline::keyPressEvent(QKeyEvent *event) QTreeWidget::keyPressEvent(event); } -void VOutline::registerNavigation(QChar p_majorKey) -{ - m_majorKey = p_majorKey; - V_ASSERT(m_keyMap.empty()); - V_ASSERT(m_naviLabels.empty()); -} - void VOutline::showNavigation() { - // Clean up. - m_keyMap.clear(); - for (auto label : m_naviLabels) { - delete label; - } - m_naviLabels.clear(); - - if (!isVisible()) { - return; - } - - // Generate labels for visible items. - auto items = getVisibleItems(); - for (int i = 0; i < 26 && i < items.size(); ++i) { - QChar key('a' + i); - m_keyMap[key] = items[i]; - - QString str = QString(m_majorKey) + key; - QLabel *label = new QLabel(str, this); - label->setStyleSheet(g_vnote->getNavigationLabelStyle(str)); - label->move(visualItemRect(items[i]).topLeft()); - label->show(); - m_naviLabels.append(label); - } -} - -void VOutline::hideNavigation() -{ - m_keyMap.clear(); - for (auto label : m_naviLabels) { - delete label; - } - m_naviLabels.clear(); + VNavigationMode::showNavigation(this); } bool VOutline::handleKeyNavigation(int p_key, bool &p_succeed) { static bool secondKey = false; - bool ret = false; - p_succeed = false; - QChar keyChar = VUtils::keyToChar(p_key); - if (secondKey && !keyChar.isNull()) { - secondKey = false; - p_succeed = true; - ret = true; - auto it = m_keyMap.find(keyChar); - if (it != m_keyMap.end()) { - setCurrentItem(it.value()); - setFocus(); - } - } else if (keyChar == m_majorKey) { - // Major key pressed. - // Need second key if m_keyMap is not empty. - if (m_keyMap.isEmpty()) { - p_succeed = true; - } else { - secondKey = true; - } - ret = true; - } - return ret; -} - -QList VOutline::getVisibleItems() const -{ - QList items; - for (int i = 0; i < topLevelItemCount(); ++i) { - QTreeWidgetItem *item = topLevelItem(i); - if (!item->isHidden()) { - items.append(item); - if (item->isExpanded()) { - items.append(getVisibleChildItems(item)); - } - } - } - return items; -} - -QList VOutline::getVisibleChildItems(const QTreeWidgetItem *p_item) const -{ - QList items; - if (p_item && !p_item->isHidden() && p_item->isExpanded()) { - for (int i = 0; i < p_item->childCount(); ++i) { - QTreeWidgetItem *child = p_item->child(i); - if (!child->isHidden()) { - items.append(child); - if (child->isExpanded()) { - items.append(getVisibleChildItems(child)); - } - } - } - } - return items; + return VNavigationMode::handleKeyNavigation(this, + secondKey, + p_key, + p_succeed); } const VTableOfContentItem *VOutline::getHeaderFromItem(QTreeWidgetItem *p_item) const diff --git a/src/voutline.h b/src/voutline.h index b7c6ff5e..3f21afd2 100644 --- a/src/voutline.h +++ b/src/voutline.h @@ -19,9 +19,7 @@ public: VOutline(QWidget *parent = 0); // Implementations for VNavigationMode. - void registerNavigation(QChar p_majorKey) Q_DECL_OVERRIDE; void showNavigation() Q_DECL_OVERRIDE; - void hideNavigation() Q_DECL_OVERRIDE; bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE; signals: @@ -64,9 +62,6 @@ private: bool selectHeaderOne(QTreeWidgetItem *p_item, const VHeaderPointer &p_header); - QList getVisibleItems() const; - QList getVisibleChildItems(const QTreeWidgetItem *p_item) const; - // Fill the info of @p_item. void fillItem(QTreeWidgetItem *p_item, const VTableOfContentItem &p_header); @@ -79,11 +74,6 @@ private: // When true, won't emit outlineItemActivated(). bool m_muted; - - // Navigation Mode. - // Map second key to QTreeWidgetItem. - QMap m_keyMap; - QVector m_naviLabels; }; #endif // VOUTLINE_H diff --git a/src/vsnippet.cpp b/src/vsnippet.cpp new file mode 100644 index 00000000..1da0fb10 --- /dev/null +++ b/src/vsnippet.cpp @@ -0,0 +1,207 @@ +#include "vsnippet.h" + +#include +#include + +#include "vconstants.h" +#include "utils/vutils.h" +#include "utils/veditutils.h" +#include "utils/vmetawordmanager.h" + +extern VMetaWordManager *g_mwMgr; + +const QString VSnippet::c_defaultCursorMark = "@@"; + +const QString VSnippet::c_defaultSelectionMark = "$$"; + +QVector VSnippet::s_allShortcuts; + +VSnippet::VSnippet() + : m_type(Type::PlainText), + m_cursorMark(c_defaultCursorMark) +{ +} + +VSnippet::VSnippet(const QString &p_name, + Type p_type, + const QString &p_content, + const QString &p_cursorMark, + const QString &p_selectionMark, + QChar p_shortcut) + : m_name(p_name), + m_type(p_type), + m_content(p_content), + m_cursorMark(p_cursorMark), + m_selectionMark(p_selectionMark), + m_shortcut(p_shortcut) +{ + Q_ASSERT(m_selectionMark != m_cursorMark); +} + +bool VSnippet::update(const QString &p_name, + Type p_type, + const QString &p_content, + const QString &p_cursorMark, + const QString &p_selectionMark, + QChar p_shortcut) +{ + bool updated = false; + if (m_name != p_name) { + m_name = p_name; + updated = true; + } + + if (m_type != p_type) { + m_type = p_type; + updated = true; + } + + if (m_content != p_content) { + m_content = p_content; + updated = true; + } + + if (m_cursorMark != p_cursorMark) { + m_cursorMark = p_cursorMark; + updated = true; + } + + if (m_selectionMark != p_selectionMark) { + m_selectionMark = p_selectionMark; + updated = true; + } + + if (m_shortcut != p_shortcut) { + m_shortcut = p_shortcut; + updated = true; + } + + qDebug() << "snippet" << m_name << "updated" << updated; + + return updated; +} + +QString VSnippet::typeStr(VSnippet::Type p_type) +{ + switch (p_type) { + case Type::PlainText: + return QObject::tr("PlainText"); + + case Type::Html: + return QObject::tr("Html"); + + default: + return QObject::tr("Invalid"); + } +} + +QJsonObject VSnippet::toJson() const +{ + QJsonObject snip; + snip[SnippetConfig::c_name] = m_name; + snip[SnippetConfig::c_type] = (int)m_type; + snip[SnippetConfig::c_cursorMark] = m_cursorMark; + snip[SnippetConfig::c_selectionMark] = m_selectionMark; + snip[SnippetConfig::c_shortcut] = m_shortcut.isNull() ? "" : QString(m_shortcut); + + return snip; +} + +VSnippet VSnippet::fromJson(const QJsonObject &p_json) +{ + QChar shortcut; + QString shortcutStr = p_json[SnippetConfig::c_shortcut].toString(); + if (!shortcutStr.isEmpty() && isValidShortcut(shortcutStr[0])) { + shortcut = shortcutStr[0]; + } + + VSnippet snip(p_json[SnippetConfig::c_name].toString(), + static_cast(p_json[SnippetConfig::c_type].toInt()), + "", + p_json[SnippetConfig::c_cursorMark].toString(), + p_json[SnippetConfig::c_selectionMark].toString(), + shortcut); + + return snip; +} + +const QVector &VSnippet::getAllShortcuts() +{ + if (s_allShortcuts.isEmpty()) { + // Init. + char ch = 'a'; + while (true) { + s_allShortcuts.append(ch); + if (ch == 'z') { + break; + } + + ch++; + } + } + + return s_allShortcuts; +} + +bool VSnippet::isValidShortcut(QChar p_char) +{ + if (p_char >= 'a' && p_char <= 'z') { + return true; + } + + return false; +} + +bool VSnippet::apply(QTextCursor &p_cursor) const +{ + p_cursor.beginEditBlock(); + // Delete selected text. + QString selection = VEditUtils::selectedText(p_cursor); + p_cursor.removeSelectedText(); + + // Evaluate the content. + QString content = g_mwMgr->evaluate(m_content); + + // Find the cursor mark and break the content. + QString secondPart; + if (!m_cursorMark.isEmpty()) { + QStringList parts = content.split(m_cursorMark, QString::SkipEmptyParts); + Q_ASSERT(parts.size() < 3); + + content = parts[0]; + if (parts.size() == 2) { + secondPart = parts[1]; + } + } + + // Replace the selection mark. + if (!m_selectionMark.isEmpty()) { + content.replace(m_selectionMark, selection); + } + + int pos = p_cursor.position() + content.size(); + + if (!secondPart.isEmpty()) { + secondPart.replace(m_selectionMark, selection); + content += secondPart; + } + + // Insert it. + switch (m_type) { + case Type::Html: + p_cursor.insertHtml(content); + // TODO: set the position of the cursor. + break; + + case Type::PlainText: + V_FALLTHROUGH; + + default: + p_cursor.insertText(content); + p_cursor.setPosition(pos); + break; + } + + p_cursor.endEditBlock(); + return true; +} diff --git a/src/vsnippet.h b/src/vsnippet.h new file mode 100644 index 00000000..815ab2be --- /dev/null +++ b/src/vsnippet.h @@ -0,0 +1,113 @@ +#ifndef VSNIPPET_H +#define VSNIPPET_H + +#include +#include +#include + + +class VSnippet +{ +public: + enum Type + { + PlainText = 0, + Html, + Invalid + }; + + VSnippet(); + + VSnippet(const QString &p_name, + Type p_type = Type::PlainText, + const QString &p_content = QString(), + const QString &p_cursorMark = c_defaultCursorMark, + const QString &p_selectionMark = c_defaultSelectionMark, + QChar p_shortcut = QChar()); + + // Return true if there is any update. + bool update(const QString &p_name, + Type p_type, + const QString &p_content, + const QString &p_cursorMark, + const QString &p_selectionMark, + QChar p_shortcut); + + const QString &getName() const + { + return m_name; + } + + VSnippet::Type getType() const + { + return m_type; + } + + const QString &getCursorMark() const + { + return m_cursorMark; + } + + const QString &getSelectionMark() const + { + return m_selectionMark; + } + + const QString &getContent() const + { + return m_content; + } + + QChar getShortcut() const + { + return m_shortcut; + } + + void setContent(const QString &p_content) + { + m_content = p_content; + } + + // Not including m_content. + QJsonObject toJson() const; + + // Apply this snippet via @p_cursor. + bool apply(QTextCursor &p_cursor) const; + + // Not including m_content. + static VSnippet fromJson(const QJsonObject &p_json); + + static QString typeStr(VSnippet::Type p_type); + + static const QVector &getAllShortcuts(); + + static bool isValidShortcut(QChar p_char); + +private: + // File name in the snippet folder. + QString m_name; + + Type m_type; + + // Support magic word. + QString m_content; + + // String in the content that mark the position of the cursor after insertion. + // If there is no such mark in the content, the cursor should be put at the + // end of the insertion. + QString m_cursorMark; + + // Selection marks in the content will be replaced by selected text. + QString m_selectionMark; + + // Shortcut to apply this snippet. + QChar m_shortcut; + + static const QString c_defaultCursorMark; + + static const QString c_defaultSelectionMark; + + static QVector s_allShortcuts; +}; + +#endif // VSNIPPET_H diff --git a/src/vsnippetlist.cpp b/src/vsnippetlist.cpp new file mode 100644 index 00000000..171a1627 --- /dev/null +++ b/src/vsnippetlist.cpp @@ -0,0 +1,606 @@ +#include "vsnippetlist.h" + +#include + +#include "vconfigmanager.h" +#include "dialog/veditsnippetdialog.h" +#include "utils/vutils.h" +#include "utils/vimnavigationforwidget.h" +#include "dialog/vsortdialog.h" +#include "dialog/vconfirmdeletiondialog.h" +#include "vmainwindow.h" + +extern VConfigManager *g_config; + +extern VMainWindow *g_mainWin; + +const QString VSnippetList::c_infoShortcutSequence = "F2"; + +VSnippetList::VSnippetList(QWidget *p_parent) + : QWidget(p_parent) +{ + setupUI(); + + initShortcuts(); + + initActions(); + + if (!readSnippetsFromConfig()) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to read snippets from %2.") + .arg(g_config->c_dataTextStyle) + .arg(g_config->getSnippetConfigFolder()), + "", + QMessageBox::Ok, + QMessageBox::Ok, + this); + } + + updateContent(); +} + +void VSnippetList::setupUI() +{ + m_addBtn = new QPushButton(QIcon(":/resources/icons/add_snippet.svg"), ""); + m_addBtn->setToolTip(tr("New Snippet")); + m_addBtn->setProperty("FlatBtn", true); + connect(m_addBtn, &QPushButton::clicked, + this, &VSnippetList::newSnippet); + + m_locateBtn = new QPushButton(QIcon(":/resources/icons/locate_snippet.svg"), ""); + m_locateBtn->setToolTip(tr("Open Folder")); + m_locateBtn->setProperty("FlatBtn", true); + connect(m_locateBtn, &QPushButton::clicked, + this, [this]() { + makeSureFolderExist(); + QUrl url = QUrl::fromLocalFile(g_config->getSnippetConfigFolder()); + QDesktopServices::openUrl(url); + }); + + m_numLabel = new QLabel(); + + QHBoxLayout *btnLayout = new QHBoxLayout; + btnLayout->addWidget(m_addBtn); + btnLayout->addWidget(m_locateBtn); + btnLayout->addStretch(); + btnLayout->addWidget(m_numLabel); + btnLayout->setContentsMargins(0, 0, 3, 0); + + m_snippetList = new QListWidget(); + m_snippetList->setContextMenuPolicy(Qt::CustomContextMenu); + m_snippetList->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_snippetList->setEditTriggers(QAbstractItemView::SelectedClicked); + connect(m_snippetList, &QListWidget::customContextMenuRequested, + this, &VSnippetList::handleContextMenuRequested); + connect(m_snippetList, &QListWidget::itemActivated, + this, &VSnippetList::handleItemActivated); + + QVBoxLayout *mainLayout = new QVBoxLayout(); + mainLayout->addLayout(btnLayout); + mainLayout->addWidget(m_snippetList); + mainLayout->setContentsMargins(0, 0, 0, 0); + + setLayout(mainLayout); +} + +void VSnippetList::initActions() +{ + m_applyAct = new QAction(QIcon(":/resources/icons/apply_snippet.svg"), + tr("&Apply"), + this); + m_applyAct->setToolTip(tr("Insert this snippet in editor")); + connect(m_applyAct, &QAction::triggered, + this, [this]() { + QListWidgetItem *item = m_snippetList->currentItem(); + handleItemActivated(item); + }); + + m_infoAct = new QAction(QIcon(":/resources/icons/snippet_info.svg"), + tr("&Info\t%1").arg(VUtils::getShortcutText(c_infoShortcutSequence)), + this); + m_infoAct->setToolTip(tr("View and edit snippet's information")); + connect(m_infoAct, &QAction::triggered, + this, &VSnippetList::snippetInfo); + + m_deleteAct = new QAction(QIcon(":/resources/icons/delete_snippet.svg"), + tr("&Delete"), + this); + m_deleteAct->setToolTip(tr("Delete selected snippets")); + connect(m_deleteAct, &QAction::triggered, + this, &VSnippetList::deleteSelectedItems); + + m_sortAct = new QAction(QIcon(":/resources/icons/sort.svg"), + tr("&Sort"), + this); + m_sortAct->setToolTip(tr("Sort snippets manually")); + connect(m_sortAct, &QAction::triggered, + this, &VSnippetList::sortItems); +} + +void VSnippetList::initShortcuts() +{ + QShortcut *infoShortcut = new QShortcut(QKeySequence(c_infoShortcutSequence), this); + infoShortcut->setContext(Qt::WidgetWithChildrenShortcut); + connect(infoShortcut, &QShortcut::activated, + this, &VSnippetList::snippetInfo); +} + +void VSnippetList::newSnippet() +{ + QString defaultName = VUtils::getFileNameWithSequence(g_config->getSnippetConfigFolder(), + "snippet"); + + VSnippet tmpSnippet(defaultName); + QString info = tr("Magic words are supported in the content of the snippet."); + VEditSnippetDialog dialog(tr("Create Snippet"), + info, + m_snippets, + tmpSnippet, + this); + if (dialog.exec() == QDialog::Accepted) { + makeSureFolderExist(); + VSnippet snippet(dialog.getNameInput(), + dialog.getTypeInput(), + dialog.getContentInput(), + dialog.getCursorMarkInput(), + dialog.getSelectionMarkInput()); + + QString errMsg; + if (!addSnippet(snippet, &errMsg)) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to create snippet %2.") + .arg(g_config->c_dataTextStyle) + .arg(snippet.getName()), + errMsg, + QMessageBox::Ok, + QMessageBox::Ok, + this); + + updateContent(); + } + } +} + +void VSnippetList::handleContextMenuRequested(QPoint p_pos) +{ + QListWidgetItem *item = m_snippetList->itemAt(p_pos); + QMenu menu(this); + menu.setToolTipsVisible(true); + + if (item) { + int itemCount = m_snippetList->selectedItems().size(); + if (itemCount == 1) { + menu.addAction(m_applyAct); + menu.addAction(m_infoAct); + } + + menu.addAction(m_deleteAct); + } + + m_snippetList->update(); + + if (m_snippets.size() > 1) { + if (!menu.actions().isEmpty()) { + menu.addSeparator(); + } + + menu.addAction(m_sortAct); + } + + if (!menu.actions().isEmpty()) { + menu.exec(m_snippetList->mapToGlobal(p_pos)); + } +} + +void VSnippetList::handleItemActivated(QListWidgetItem *p_item) +{ + const VSnippet *snip = getSnippet(p_item); + if (snip) { + VEditTab *tab = g_mainWin->getCurrentTab(); + if (tab) { + tab->applySnippet(snip); + } + } +} + +void VSnippetList::deleteSelectedItems() +{ + QVector items; + const QList selectedItems = m_snippetList->selectedItems(); + + if (selectedItems.isEmpty()) { + return; + } + + for (auto const & item : selectedItems) { + items.push_back(ConfirmItemInfo(item->text(), + item->text(), + "", + NULL)); + } + + QString text = tr("Are you sure to delete these snippets?"); + QString info = tr("Click \"Cancel\" to leave them untouched."); + VConfirmDeletionDialog dialog(tr("Confirm Deleting Snippets"), + text, + info, + items, + false, + false, + false, + this); + if (dialog.exec()) { + items = dialog.getConfirmedItems(); + + QList names; + for (auto const & item : items) { + names.append(item.m_name); + } + + QString errMsg; + if (!deleteSnippets(names, &errMsg)) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to delete snippets."), + errMsg, + QMessageBox::Ok, + QMessageBox::Ok, + this); + } + + updateContent(); + } +} + +void VSnippetList::sortItems() +{ + if (m_snippets.size() < 2) { + return; + } + + VSortDialog dialog(tr("Sort Snippets"), + tr("Sort snippets in the configuration file."), + this); + QTreeWidget *tree = dialog.getTreeWidget(); + tree->clear(); + tree->setColumnCount(1); + QStringList headers; + headers << tr("Name"); + tree->setHeaderLabels(headers); + + for (int i = 0; i < m_snippets.size(); ++i) { + QTreeWidgetItem *item = new QTreeWidgetItem(tree, QStringList(m_snippets[i].getName())); + + item->setData(0, Qt::UserRole, i); + } + + dialog.treeUpdated(); + + if (dialog.exec()) { + QVector data = dialog.getSortedData(); + Q_ASSERT(data.size() == m_snippets.size()); + QVector sortedIdx(data.size(), -1); + for (int i = 0; i < data.size(); ++i) { + sortedIdx[i] = data[i].toInt(); + } + + QString errMsg; + if (!sortSnippets(sortedIdx, &errMsg)) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to sort snippets."), + errMsg, + QMessageBox::Ok, + QMessageBox::Ok, + this); + } + + updateContent(); + } +} + +void VSnippetList::snippetInfo() +{ + if (m_snippetList->selectedItems().size() != 1) { + return; + } + + QListWidgetItem *item = m_snippetList->currentItem(); + VSnippet *snip = getSnippet(item); + if (!snip) { + return; + } + + QString info = tr("Magic words are supported in the content of the snippet."); + VEditSnippetDialog dialog(tr("Snippet Information"), + info, + m_snippets, + *snip, + this); + if (dialog.exec() == QDialog::Accepted) { + QString errMsg; + bool ret = true; + if (snip->getName() != dialog.getNameInput()) { + // Delete the original snippet file. + if (!deleteSnippetFile(*snip, &errMsg)) { + ret = false; + } + } + + if (snip->update(dialog.getNameInput(), + dialog.getTypeInput(), + dialog.getContentInput(), + dialog.getCursorMarkInput(), + dialog.getSelectionMarkInput(), + dialog.getShortcutInput())) { + if (!writeSnippetFile(*snip, &errMsg)) { + ret = false; + } + + if (!writeSnippetsToConfig()) { + VUtils::addErrMsg(&errMsg, + tr("Fail to write snippets configuration file.")); + ret = false; + } + + updateContent(); + } + + if (!ret) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to update information of snippet %2.") + .arg(g_config->c_dataTextStyle) + .arg(snip->getName()), + errMsg, + QMessageBox::Ok, + QMessageBox::Ok, + this); + } + } +} + +void VSnippetList::makeSureFolderExist() const +{ + QString path = g_config->getSnippetConfigFolder(); + if (!QFileInfo::exists(path)) { + QDir dir; + dir.mkpath(path); + } +} + +void VSnippetList::updateContent() +{ + m_snippetList->clear(); + + for (int i = 0; i < m_snippets.size(); ++i) { + const VSnippet &snip = m_snippets[i]; + QListWidgetItem *item = new QListWidgetItem(snip.getName()); + item->setToolTip(snip.getName()); + item->setData(Qt::UserRole, snip.getName()); + + m_snippetList->addItem(item); + } + + int cnt = m_snippetList->count(); + if (cnt > 0) { + m_numLabel->setText(tr("%1 %2").arg(cnt) + .arg(cnt > 1 ? tr("Snippets") : tr("Snippet"))); + m_snippetList->setFocus(); + } else { + m_numLabel->setText(""); + m_addBtn->setFocus(); + } +} + +bool VSnippetList::addSnippet(const VSnippet &p_snippet, QString *p_errMsg) +{ + if (!writeSnippetFile(p_snippet, p_errMsg)) { + return false; + } + + m_snippets.push_back(p_snippet); + + bool ret = true; + if (!writeSnippetsToConfig()) { + VUtils::addErrMsg(p_errMsg, + tr("Fail to write snippets configuration file.")); + m_snippets.pop_back(); + ret = false; + } + + updateContent(); + + return ret; +} + +bool VSnippetList::writeSnippetsToConfig() const +{ + makeSureFolderExist(); + + QJsonObject snippetJson; + snippetJson[SnippetConfig::c_version] = "1"; + + QJsonArray snippetArray; + for (int i = 0; i < m_snippets.size(); ++i) { + snippetArray.append(m_snippets[i].toJson()); + } + + snippetJson[SnippetConfig::c_snippets] = snippetArray; + + return VUtils::writeJsonToDisk(g_config->getSnippetConfigFilePath(), + snippetJson); +} + +bool VSnippetList::readSnippetsFromConfig() +{ + m_snippets.clear(); + + if (!QFileInfo::exists(g_config->getSnippetConfigFilePath())) { + return true; + } + + QJsonObject snippets = VUtils::readJsonFromDisk(g_config->getSnippetConfigFilePath()); + if (snippets.isEmpty()) { + qWarning() << "invalid snippets configuration file" << g_config->getSnippetConfigFilePath(); + return false; + } + + // [snippets] section. + bool ret = true; + QJsonArray snippetArray = snippets[SnippetConfig::c_snippets].toArray(); + for (int i = 0; i < snippetArray.size(); ++i) { + VSnippet snip = VSnippet::fromJson(snippetArray[i].toObject()); + + // Read the content. + QString filePath(QDir(g_config->getSnippetConfigFolder()).filePath(snip.getName())); + QString content = VUtils::readFileFromDisk(filePath); + if (content.isNull()) { + qWarning() << "fail to read snippet" << snip.getName(); + ret = false; + continue; + } + + snip.setContent(content); + m_snippets.push_back(snip); + } + + return ret; +} + +void VSnippetList::keyPressEvent(QKeyEvent *p_event) +{ + if (VimNavigationForWidget::injectKeyPressEventForVim(m_snippetList, + p_event)) { + return; + } + + QWidget::keyPressEvent(p_event); +} + +void VSnippetList::showNavigation() +{ + VNavigationMode::showNavigation(m_snippetList); +} + +bool VSnippetList::handleKeyNavigation(int p_key, bool &p_succeed) +{ + static bool secondKey = false; + return VNavigationMode::handleKeyNavigation(m_snippetList, + secondKey, + p_key, + p_succeed); +} + +int VSnippetList::getSnippetIndex(QListWidgetItem *p_item) const +{ + if (!p_item) { + return -1; + } + + QString name = p_item->data(Qt::UserRole).toString(); + for (int i = 0; i < m_snippets.size(); ++i) { + if (m_snippets[i].getName() == name) { + return i; + } + } + + Q_ASSERT(false); + return -1; +} + +VSnippet *VSnippetList::getSnippet(QListWidgetItem *p_item) +{ + int idx = getSnippetIndex(p_item); + if (idx == -1) { + return NULL; + } else { + return &m_snippets[idx]; + } +} + +bool VSnippetList::writeSnippetFile(const VSnippet &p_snippet, QString *p_errMsg) +{ + // Create and write to the snippet file. + QString filePath = getSnippetFilePath(p_snippet); + if (!VUtils::writeFileToDisk(filePath, p_snippet.getContent())) { + VUtils::addErrMsg(p_errMsg, + tr("Fail to add write the snippet file %1.") + .arg(filePath)); + return false; + } + + return true; +} + +QString VSnippetList::getSnippetFilePath(const VSnippet &p_snippet) const +{ + return QDir(g_config->getSnippetConfigFolder()).filePath(p_snippet.getName()); +} + +bool VSnippetList::sortSnippets(const QVector &p_sortedIdx, QString *p_errMsg) +{ + V_ASSERT(p_sortedIdx.size() == m_snippets.size()); + + auto ori = m_snippets; + + for (int i = 0; i < p_sortedIdx.size(); ++i) { + m_snippets[i] = ori[p_sortedIdx[i]]; + } + + bool ret = true; + if (!writeSnippetsToConfig()) { + VUtils::addErrMsg(p_errMsg, + tr("Fail to write snippets configuration file.")); + m_snippets = ori; + ret = false; + } + + return ret; +} + +bool VSnippetList::deleteSnippets(const QList &p_snippets, + QString *p_errMsg) +{ + if (p_snippets.isEmpty()) { + return true; + } + + bool ret = true; + QSet targets = QSet::fromList(p_snippets); + for (auto it = m_snippets.begin(); it != m_snippets.end();) { + if (targets.contains(it->getName())) { + // Remove it. + if (!deleteSnippetFile(*it, p_errMsg)) { + ret = false; + } + + it = m_snippets.erase(it); + } else { + ++it; + } + } + + if (!writeSnippetsToConfig()) { + VUtils::addErrMsg(p_errMsg, + tr("Fail to write snippets configuration file.")); + ret = false; + } + + return ret; +} + +bool VSnippetList::deleteSnippetFile(const VSnippet &p_snippet, QString *p_errMsg) +{ + QString filePath = getSnippetFilePath(p_snippet); + if (!VUtils::deleteFile(filePath)) { + VUtils::addErrMsg(p_errMsg, + tr("Fail to remove snippet file %1.") + .arg(filePath)); + return false; + } + + return true; +} diff --git a/src/vsnippetlist.h b/src/vsnippetlist.h new file mode 100644 index 00000000..aef5463f --- /dev/null +++ b/src/vsnippetlist.h @@ -0,0 +1,98 @@ +#ifndef VSNIPPETLIST_H +#define VSNIPPETLIST_H + +#include +#include +#include + +#include "vsnippet.h" +#include "vnavigationmode.h" + +class QPushButton; +class QListWidget; +class QListWidgetItem; +class QLabel; +class QAction; +class QKeyEvent; + + +class VSnippetList : public QWidget, public VNavigationMode +{ + Q_OBJECT +public: + explicit VSnippetList(QWidget *p_parent = nullptr); + + // Implementations for VNavigationMode. + void showNavigation() Q_DECL_OVERRIDE; + bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE; + +protected: + void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; + +private slots: + void newSnippet(); + + void handleContextMenuRequested(QPoint p_pos); + + void handleItemActivated(QListWidgetItem *p_item); + + void snippetInfo(); + + void deleteSelectedItems(); + + void sortItems(); + +private: + void setupUI(); + + void initActions(); + + void initShortcuts(); + + void makeSureFolderExist() const; + + // Update list of snippets according to m_snippets. + void updateContent(); + + // Add @p_snippet. + bool addSnippet(const VSnippet &p_snippet, QString *p_errMsg = nullptr); + + // Write m_snippets to config file. + bool writeSnippetsToConfig() const; + + // Read from config file to m_snippets. + bool readSnippetsFromConfig(); + + // Get the snippet index in m_snippets of @p_item. + int getSnippetIndex(QListWidgetItem *p_item) const; + + VSnippet *getSnippet(QListWidgetItem *p_item); + + // Write the content of @p_snippet to file. + bool writeSnippetFile(const VSnippet &p_snippet, QString *p_errMsg = nullptr); + + QString getSnippetFilePath(const VSnippet &p_snippet) const; + + // Sort m_snippets according to @p_sortedIdx. + bool sortSnippets(const QVector &p_sortedIdx, QString *p_errMsg = nullptr); + + bool deleteSnippets(const QList &p_snippets, QString *p_errMsg = nullptr); + + bool deleteSnippetFile(const VSnippet &p_snippet, QString *p_errMsg = nullptr); + + QPushButton *m_addBtn; + QPushButton *m_locateBtn; + QLabel *m_numLabel; + QListWidget *m_snippetList; + + QAction *m_applyAct; + QAction *m_infoAct; + QAction *m_deleteAct; + QAction *m_sortAct; + + QVector m_snippets; + + static const QString c_infoShortcutSequence; +}; + +#endif // VSNIPPETLIST_H