Browse Source

snippet: support Ctrl+E S to insert snippets

Le Tan 8 years ago
parent
commit
0a97b2480d

+ 2 - 0
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.
 

+ 2 - 0
src/resources/docs/shortcuts_zh.md

@@ -219,6 +219,8 @@ RemoveSplit=R
 将当前标签页左移一个分割窗口。
 - `Shift+L`  
 将当前标签页右移一个分割窗口。
+- `S`  
+在编辑模式中应用片段。
 - `?`   
 显示本快捷键说明。
 

+ 2 - 0
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

+ 9 - 0
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;
 }

+ 4 - 2
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 \

+ 1 - 0
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();
 }
 

+ 2 - 1
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]() {

+ 0 - 19
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
 {

+ 14 - 0
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<VEditArea *>(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) {

+ 3 - 0
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;

+ 4 - 0
src/vedittab.cpp

@@ -129,3 +129,7 @@ void VEditTab::applySnippet(const VSnippet *p_snippet)
 {
     Q_UNUSED(p_snippet);
 }
+
+void VEditTab::applySnippet()
+{
+}

+ 3 - 0
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;

+ 113 - 0
src/vinsertselector.cpp

@@ -0,0 +1,113 @@
+#include "vinsertselector.h"
+
+#include <QtWidgets>
+#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<VInsertSelectorItem> &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 &it;
+        }
+    }
+
+    return NULL;
+}
+
+void VInsertSelector::showEvent(QShowEvent *p_event)
+{
+    QWidget::showEvent(p_event);
+
+    if (!hasFocus()) {
+        setFocus();
+    }
+}

+ 87 - 0
src/vinsertselector.h

@@ -0,0 +1,87 @@
+#ifndef VINSERTSELECTOR_H
+#define VINSERTSELECTOR_H
+
+#include <QWidget>
+#include <QVector>
+
+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<VInsertSelectorItem> &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<VInsertSelectorItem> m_items;
+
+    QString m_clickedItemName;
+};
+
+inline const QString &VInsertSelector::getClickedItem() const
+{
+    return m_clickedItemName;
+}
+
+#endif // VINSERTSELECTOR_H

+ 7 - 0
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

+ 78 - 1
src/vmdtab.cpp

@@ -1,7 +1,7 @@
 #include <QtWidgets>
 #include <QWebChannel>
 #include <QFileInfo>
-#include <QXmlStreamReader>
+#include <QCoreApplication>
 #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<VInsertSelectorItem> 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;
+}

+ 6 - 0
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;

+ 10 - 4
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;
     }

+ 9 - 4
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());
 

+ 19 - 0
src/vsnippetlist.h

@@ -23,6 +23,10 @@ class VSnippetList : public QWidget, public VNavigationMode
 public:
     explicit VSnippetList(QWidget *p_parent = nullptr);
 
+    const QVector<VSnippet> &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<VSnippet> &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