diff --git a/src/resources/docs/shortcuts_en.md b/src/resources/docs/shortcuts_en.md index 7abd6b85..14821bf8 100644 --- a/src/resources/docs/shortcuts_en.md +++ b/src/resources/docs/shortcuts_en.md @@ -218,6 +218,8 @@ Jump to the first split window on the right. Move current tab one split window left. - `Shift+L` Move current tab one split window right. +- `S` +Apply a snippet in edit mode. - `?` Display shortcuts documentation. diff --git a/src/resources/docs/shortcuts_zh.md b/src/resources/docs/shortcuts_zh.md index 174dfc8e..c71046de 100644 --- a/src/resources/docs/shortcuts_zh.md +++ b/src/resources/docs/shortcuts_zh.md @@ -219,6 +219,8 @@ RemoveSplit=R 将当前标签页左移一个分割窗口。 - `Shift+L` 将当前标签页右移一个分割窗口。 +- `S` +在编辑模式中应用片段。 - `?` 显示本快捷键说明。 diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index a4aff38e..364616ce 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -255,3 +255,5 @@ VerticalSplit=V RemoveSplit=R ; Evaluate selected text or cursor word as magic words MagicWord=M +; Prompt for user to apply a snippet +ApplySnippet=S diff --git a/src/resources/vnote.qss b/src/resources/vnote.qss index 45ad190e..305268ac 100644 --- a/src/resources/vnote.qss +++ b/src/resources/vnote.qss @@ -268,6 +268,15 @@ QLabel[ColorTealLabel="true"] { background-color: @Teal7; } +VSelectorItemWidget QLabel[SelectorItemShortcutLabel="true"] { + font: bold; + border: 2px solid @logo-min; + padding: 3px; + border-radius: 5px; + background-color: @logo-base; + color: @logo-max; +} + QWidget[NotebookPanel="true"] { padding-left: 3px; } diff --git a/src/src.pro b/src/src.pro index 22128bd3..67114f9b 100644 --- a/src/src.pro +++ b/src/src.pro @@ -97,7 +97,8 @@ SOURCES += main.cpp\ vsnippet.cpp \ dialog/veditsnippetdialog.cpp \ utils/vimnavigationforwidget.cpp \ - vtoolbox.cpp + vtoolbox.cpp \ + vinsertselector.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -181,7 +182,8 @@ HEADERS += vmainwindow.h \ vsnippet.h \ dialog/veditsnippetdialog.h \ utils/vimnavigationforwidget.h \ - vtoolbox.h + vtoolbox.h \ + vinsertselector.h RESOURCES += \ vnote.qrc \ diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index ee0be391..3a5ae0f3 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -516,6 +516,7 @@ QChar VUtils::keyToChar(int p_key) if (p_key >= Qt::Key_A && p_key <= Qt::Key_Z) { return QChar('a' + p_key - Qt::Key_A); } + return QChar(); } diff --git a/src/vbuttonwithwidget.cpp b/src/vbuttonwithwidget.cpp index 0506d4a1..2f036dff 100644 --- a/src/vbuttonwithwidget.cpp +++ b/src/vbuttonwithwidget.cpp @@ -41,7 +41,8 @@ void VButtonWithWidget::init() m_bubbleBg = QColor("#15AE67"); QMenu *menu = new QMenu(this); - VButtonWidgetAction *act = new VButtonWidgetAction(m_popupWidget, menu); + QWidgetAction *act = new QWidgetAction(menu); + act->setDefaultWidget(m_popupWidget); menu->addAction(act); connect(menu, &QMenu::aboutToShow, this, [this]() { diff --git a/src/vbuttonwithwidget.h b/src/vbuttonwithwidget.h index 42c3be50..84929df1 100644 --- a/src/vbuttonwithwidget.h +++ b/src/vbuttonwithwidget.h @@ -39,25 +39,6 @@ private: VButtonWithWidget *m_btn; }; -class VButtonWidgetAction : public QWidgetAction -{ - Q_OBJECT -public: - VButtonWidgetAction(QWidget *p_widget, QWidget *p_parent) - : QWidgetAction(p_parent), m_widget(p_widget) - { - } - - QWidget *createWidget(QWidget *p_parent) - { - m_widget->setParent(p_parent); - return m_widget; - } - -private: - QWidget *m_widget; -}; - // A QPushButton with popup widget. class VButtonWithWidget : public QPushButton { diff --git a/src/veditarea.cpp b/src/veditarea.cpp index 7f5e2267..3010f851 100644 --- a/src/veditarea.cpp +++ b/src/veditarea.cpp @@ -885,6 +885,10 @@ void VEditArea::registerCaptainTargets() g_config->getCaptainShortcutKeySequence("MagicWord"), this, evaluateMagicWordsByCaptain); + captain->registerCaptainTarget(tr("ApplySnippet"), + g_config->getCaptainShortcutKeySequence("ApplySnippet"), + this, + applySnippetByCaptain); } void VEditArea::activateTabByCaptain(void *p_target, void *p_data, int p_idx) @@ -989,6 +993,16 @@ void VEditArea::evaluateMagicWordsByCaptain(void *p_target, void *p_data) } } +void VEditArea::applySnippetByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + VEditTab *tab = obj->getCurrentTab(); + if (tab && tab->tabHasFocus()) { + tab->applySnippet(); + } +} + void VEditArea::recordClosedFile(const VFileSessionInfo &p_file) { for (auto it = m_lastClosedFiles.begin(); it != m_lastClosedFiles.end(); ++it) { diff --git a/src/veditarea.h b/src/veditarea.h index 4c621e4b..d95a63be 100644 --- a/src/veditarea.h +++ b/src/veditarea.h @@ -195,6 +195,9 @@ private: // Evaluate selected text or the word on cursor as magic words. static void evaluateMagicWordsByCaptain(void *p_target, void *p_data); + // Prompt for user to apply a snippet. + static void applySnippetByCaptain(void *p_target, void *p_data); + // End Captain mode functions. int curWindowIndex; diff --git a/src/vedittab.cpp b/src/vedittab.cpp index 04dc8387..6796830e 100644 --- a/src/vedittab.cpp +++ b/src/vedittab.cpp @@ -129,3 +129,7 @@ void VEditTab::applySnippet(const VSnippet *p_snippet) { Q_UNUSED(p_snippet); } + +void VEditTab::applySnippet() +{ +} diff --git a/src/vedittab.h b/src/vedittab.h index d04742fb..44885eb4 100644 --- a/src/vedittab.h +++ b/src/vedittab.h @@ -95,6 +95,9 @@ public: // Insert snippet @p_snippet. virtual void applySnippet(const VSnippet *p_snippet); + // Prompt for user to apply a snippet. + virtual void applySnippet(); + public slots: // Enter edit mode virtual void editFile() = 0; diff --git a/src/vinsertselector.cpp b/src/vinsertselector.cpp new file mode 100644 index 00000000..4bc5e766 --- /dev/null +++ b/src/vinsertselector.cpp @@ -0,0 +1,113 @@ +#include "vinsertselector.h" + +#include +#include "utils/vutils.h" + +VSelectorItemWidget::VSelectorItemWidget(QWidget *p_parent) + : QWidget(p_parent) +{ +} + +VSelectorItemWidget::VSelectorItemWidget(const VInsertSelectorItem &p_item, QWidget *p_parent) + : QWidget(p_parent), m_name(p_item.m_name) +{ + QLabel *shortcutLabel = new QLabel(p_item.m_shortcut); + shortcutLabel->setProperty("SelectorItemShortcutLabel", true); + + m_btn = new QPushButton(p_item.m_name); + m_btn->setToolTip(p_item.m_toolTip); + m_btn->setProperty("SelectionBtn", true); + connect(m_btn, &QPushButton::clicked, + this, [this]() { + emit clicked(m_name); + }); + + QHBoxLayout *layout = new QHBoxLayout(); + layout->addWidget(shortcutLabel); + layout->addWidget(m_btn, 1); + layout->setContentsMargins(0, 0, 0, 0); + + setLayout(layout); +} + +VInsertSelector::VInsertSelector(int p_nrRows, + const QVector &p_items, + QWidget *p_parent) + : QWidget(p_parent), + m_items(p_items) +{ + setupUI(p_nrRows < 1 ? 1 : p_nrRows); +} + +void VInsertSelector::setupUI(int p_nrRows) +{ + QGridLayout *layout = new QGridLayout(); + + int row = 0, col = 0; + for (auto const & it : m_items) { + QWidget *wid = createItemWidget(it); + layout->addWidget(wid, row, col); + if (++row == p_nrRows) { + row = 0; + ++col; + } + } + + setLayout(layout); +} + +QWidget *VInsertSelector::createItemWidget(const VInsertSelectorItem &p_item) +{ + VSelectorItemWidget *widget = new VSelectorItemWidget(p_item); + connect(widget, &VSelectorItemWidget::clicked, + this, &VInsertSelector::itemClicked); + + return widget; +} + +void VInsertSelector::itemClicked(const QString &p_name) +{ + m_clickedItemName = p_name; + emit accepted(true); +} + +void VInsertSelector::keyPressEvent(QKeyEvent *p_event) +{ + QWidget::keyPressEvent(p_event); + + if (p_event->key() == Qt::Key_BracketLeft + && VUtils::isControlModifierForVim(p_event->modifiers())) { + m_clickedItemName.clear(); + emit accepted(false); + return; + } + + QChar ch = VUtils::keyToChar(p_event->key()); + if (!ch.isNull()) { + // Activate corresponding item. + const VInsertSelectorItem *item = findItemByShortcut(ch); + if (item) { + itemClicked(item->m_name); + } + } +} + +const VInsertSelectorItem *VInsertSelector::findItemByShortcut(QChar p_shortcut) const +{ + for (auto const & it : m_items) { + if (it.m_shortcut == p_shortcut) { + return ⁢ + } + } + + return NULL; +} + +void VInsertSelector::showEvent(QShowEvent *p_event) +{ + QWidget::showEvent(p_event); + + if (!hasFocus()) { + setFocus(); + } +} diff --git a/src/vinsertselector.h b/src/vinsertselector.h new file mode 100644 index 00000000..04988199 --- /dev/null +++ b/src/vinsertselector.h @@ -0,0 +1,87 @@ +#ifndef VINSERTSELECTOR_H +#define VINSERTSELECTOR_H + +#include +#include + +class QPushButton; +class QKeyEvent; +class QShowEvent; + +struct VInsertSelectorItem +{ + VInsertSelectorItem() + { + } + + VInsertSelectorItem(const QString &p_name, + const QString &p_toolTip, + QChar p_shortcut = QChar()) + : m_name(p_name), m_toolTip(p_toolTip), m_shortcut(p_shortcut) + { + } + + QString m_name; + + QString m_toolTip; + + QChar m_shortcut; +}; + +class VSelectorItemWidget : public QWidget +{ + Q_OBJECT +public: + explicit VSelectorItemWidget(QWidget *p_parent = nullptr); + + VSelectorItemWidget(const VInsertSelectorItem &p_item, QWidget *p_parent = nullptr); + +signals: + // This item widget is clicked. + void clicked(const QString &p_name); + +private: + QString m_name; + + QPushButton *m_btn; +}; + +class VInsertSelector : public QWidget +{ + Q_OBJECT +public: + explicit VInsertSelector(int p_nrRows, + const QVector &p_items, + QWidget *p_parent = nullptr); + + const QString &getClickedItem() const; + +signals: + void accepted(bool p_accepted = true); + +protected: + void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; + + void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE; + +private slots: + void itemClicked(const QString &p_name); + +private: + void setupUI(int p_nrRows); + + QWidget *createItemWidget(const VInsertSelectorItem &p_item); + + const VInsertSelectorItem *findItemByShortcut(QChar p_shortcut) const; + + QVector m_items; + + QString m_clickedItemName; +}; + +inline const QString &VInsertSelector::getClickedItem() const +{ + return m_clickedItemName; +} + +#endif // VINSERTSELECTOR_H diff --git a/src/vmainwindow.h b/src/vmainwindow.h index 7cb88878..39a8f0d7 100644 --- a/src/vmainwindow.h +++ b/src/vmainwindow.h @@ -64,6 +64,8 @@ public: VEditArea *getEditArea() const; + VSnippetList *getSnippetList() const; + // View and edit the information of @p_file, which is an orphan file. void editOrphanFileInfo(VFile *p_file); @@ -411,4 +413,9 @@ inline VEditTab *VMainWindow::getCurrentTab() const return m_curTab; } +inline VSnippetList *VMainWindow::getSnippetList() const +{ + return m_snippetList; +} + #endif // VMAINWINDOW_H diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index 1310e350..611a0e14 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include "vmdtab.h" #include "vdocument.h" #include "vnote.h" @@ -19,6 +19,8 @@ #include "vmdeditor.h" #include "vmainwindow.h" #include "vsnippet.h" +#include "vinsertselector.h" +#include "vsnippetlist.h" extern VMainWindow *g_mainWin; @@ -728,6 +730,8 @@ void VMdTab::evaluateMagicWords() void VMdTab::applySnippet(const VSnippet *p_snippet) { + Q_ASSERT(p_snippet); + if (isEditMode() && m_file->isModifiable() && p_snippet->getType() == VSnippet::Type::PlainText) { @@ -738,6 +742,79 @@ void VMdTab::applySnippet(const VSnippet *p_snippet) m_editor->setTextCursor(cursor); m_editor->setVimMode(VimMode::Insert); + + g_mainWin->showStatusMessage(tr("Snippet applied")); + } + } else { + g_mainWin->showStatusMessage(tr("Snippet %1 is not applicable").arg(p_snippet->getName())); + } +} + +void VMdTab::applySnippet() +{ + if (!isEditMode() || !m_file->isModifiable()) { + g_mainWin->showStatusMessage(tr("Snippets are not applicable")); + return; + } + + QPoint pos(m_editor->cursorRect().bottomRight()); + QMenu menu(this); + VInsertSelector *sel = prepareSnippetSelector(&menu); + if (!sel) { + g_mainWin->showStatusMessage(tr("No available snippets defined with shortcuts")); + return; + } + + QWidgetAction *act = new QWidgetAction(&menu); + act->setDefaultWidget(sel); + connect(sel, &VInsertSelector::accepted, + this, [this, &menu]() { + QKeyEvent *escEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Escape, + Qt::NoModifier); + QCoreApplication::postEvent(&menu, escEvent); + }); + + menu.addAction(act); + + menu.exec(m_editor->mapToGlobal(pos)); + + QString chosenItem = sel->getClickedItem(); + if (!chosenItem.isEmpty()) { + const VSnippet *snip = g_mainWin->getSnippetList()->getSnippet(chosenItem); + if (snip) { + applySnippet(snip); } } } + +static bool selectorItemCmp(const VInsertSelectorItem &p_a, const VInsertSelectorItem &p_b) +{ + if (p_a.m_shortcut < p_b.m_shortcut) { + return true; + } + + return false; +} + +VInsertSelector *VMdTab::prepareSnippetSelector(QWidget *p_parent) +{ + auto snippets = g_mainWin->getSnippetList()->getSnippets(); + QVector items; + for (auto const & snip : snippets) { + if (!snip.getShortcut().isNull()) { + items.push_back(VInsertSelectorItem(snip.getName(), + snip.getName(), + snip.getShortcut())); + } + } + + if (items.isEmpty()) { + return NULL; + } + + // Sort items by shortcut. + std::sort(items.begin(), items.end(), selectorItemCmp); + + VInsertSelector *sel = new VInsertSelector(7, items, p_parent); + return sel; +} diff --git a/src/vmdtab.h b/src/vmdtab.h index 922edc1e..c4372292 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -12,6 +12,7 @@ class VWebView; class QStackedLayout; class VDocument; class VMdEditor; +class VInsertSelector; class VMdTab : public VEditTab { @@ -77,6 +78,8 @@ public: void applySnippet(const VSnippet *p_snippet) Q_DECL_OVERRIDE; + void applySnippet() Q_DECL_OVERRIDE; + public slots: // Enter edit mode. void editFile() Q_DECL_OVERRIDE; @@ -156,6 +159,9 @@ private: // Return true if succeed. bool restoreFromTabInfo(const VEditTabInfo &p_info) Q_DECL_OVERRIDE; + // Prepare insert selector with snippets. + VInsertSelector *prepareSnippetSelector(QWidget *p_parent = nullptr); + VMdEditor *m_editor; VWebView *m_webViewer; VDocument *m_document; diff --git a/src/vsnippet.cpp b/src/vsnippet.cpp index 1da0fb10..1c987107 100644 --- a/src/vsnippet.cpp +++ b/src/vsnippet.cpp @@ -162,11 +162,16 @@ bool VSnippet::apply(QTextCursor &p_cursor) const // Evaluate the content. QString content = g_mwMgr->evaluate(m_content); + if (content.isEmpty()) { + p_cursor.endEditBlock(); + return true; + } + // 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); + QStringList parts = content.split(m_cursorMark); + Q_ASSERT(parts.size() < 3 && parts.size() > 0); content = parts[0]; if (parts.size() == 2) { @@ -175,13 +180,14 @@ bool VSnippet::apply(QTextCursor &p_cursor) const } // Replace the selection mark. - if (!m_selectionMark.isEmpty()) { + // Content may be empty. + if (!m_selectionMark.isEmpty() && !content.isEmpty()) { content.replace(m_selectionMark, selection); } int pos = p_cursor.position() + content.size(); - if (!secondPart.isEmpty()) { + if (!m_selectionMark.isEmpty() && !secondPart.isEmpty()) { secondPart.replace(m_selectionMark, selection); content += secondPart; } diff --git a/src/vsnippetlist.cpp b/src/vsnippetlist.cpp index a2b49a7b..0822f416 100644 --- a/src/vsnippetlist.cpp +++ b/src/vsnippetlist.cpp @@ -144,7 +144,8 @@ void VSnippetList::newSnippet() dialog.getTypeInput(), dialog.getContentInput(), dialog.getCursorMarkInput(), - dialog.getSelectionMarkInput()); + dialog.getSelectionMarkInput(), + dialog.getShortcutInput()); QString errMsg; if (!addSnippet(snippet, &errMsg)) { @@ -215,8 +216,9 @@ void VSnippetList::deleteSelectedItems() } for (auto const & item : selectedItems) { - items.push_back(ConfirmItemInfo(item->text(), - item->text(), + QString name = item->data(Qt::UserRole).toString(); + items.push_back(ConfirmItemInfo(name, + name, "", NULL)); } @@ -377,7 +379,10 @@ void VSnippetList::updateContent() for (int i = 0; i < m_snippets.size(); ++i) { const VSnippet &snip = m_snippets[i]; - QListWidgetItem *item = new QListWidgetItem(snip.getName()); + QString text = QString("%1%2").arg(snip.getName()) + .arg(snip.getShortcut().isNull() + ? "" : QString(" [%1]").arg(snip.getShortcut())); + QListWidgetItem *item = new QListWidgetItem(text); item->setToolTip(snip.getName()); item->setData(Qt::UserRole, snip.getName()); diff --git a/src/vsnippetlist.h b/src/vsnippetlist.h index 9bdbc1f8..daa307f1 100644 --- a/src/vsnippetlist.h +++ b/src/vsnippetlist.h @@ -23,6 +23,10 @@ class VSnippetList : public QWidget, public VNavigationMode public: explicit VSnippetList(QWidget *p_parent = nullptr); + const QVector &getSnippets() const; + + const VSnippet *getSnippet(const QString &p_name) const; + // Implementations for VNavigationMode. void showNavigation() Q_DECL_OVERRIDE; bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE; @@ -98,4 +102,19 @@ private: static const QString c_infoShortcutSequence; }; +inline const QVector &VSnippetList::getSnippets() const +{ + return m_snippets; +} + +inline const VSnippet *VSnippetList::getSnippet(const QString &p_name) const +{ + for (auto const & snip : m_snippets) { + if (snip.getName() == p_name) { + return &snip; + } + } + + return NULL; +} #endif // VSNIPPETLIST_H