1
0
Le Tan 4 жил өмнө
parent
commit
58e8ea5ee8

+ 1 - 1
libs/vtextedit

@@ -1 +1 @@
-Subproject commit 98274148a0e1ad371f29abe072fac35bf5d7b6df
+Subproject commit 7045758b2c9c10f6b72b97f15c40ded97db6ac0d

+ 1 - 0
src/core/editorconfig.h

@@ -49,6 +49,7 @@ namespace vnotex
             FindAndReplace,
             FindNext,
             FindPrevious,
+            ApplySnippet,
             MaxShortcut
         };
         Q_ENUM(Shortcut)

+ 1 - 0
src/core/mainconfig.cpp

@@ -117,4 +117,5 @@ QString MainConfig::getVersion(const QJsonObject &p_jobj)
 void MainConfig::doVersionSpecificOverride()
 {
     // In a new version, we may want to change one value by force.
+    m_coreConfig->m_shortcuts[CoreConfig::Shortcut::SearchDock].clear();
 }

+ 4 - 3
src/data/core/vnotex.json

@@ -17,8 +17,8 @@
             "CloseTab" : "Ctrl+G, X",
             "NavigationDock" : "Ctrl+G, A",
             "OutlineDock" : "Ctrl+G, U",
-            "SearchDock" : "Ctrl+G, S",
-            "SnippetDock" : "",
+            "SearchDock" : "",
+            "SnippetDock" : "Ctrl+G, S",
             "LocationListDock" : "Ctrl+G, L",
             "Search" : "Ctrl+Alt+F",
             "NavigationMode" : "Ctrl+G, W",
@@ -93,7 +93,8 @@
                 "RichPaste" : "Ctrl+Shift+V",
                 "FindAndReplace" : "Ctrl+F",
                 "FindNext" : "F3",
-                "FindPrevious" : "Shift+F3"
+                "FindPrevious" : "Shift+F3",
+                "ApplySnippet" : "Ctrl+G, I"
             },
             "spell_check_auto_detect_language" : false,
             "spell_check_default_dictionary" : "en_US"

+ 1 - 0
src/snippet/dynamicsnippet.cpp

@@ -9,6 +9,7 @@ DynamicSnippet::DynamicSnippet(const QString &p_name,
                                const Callback &p_callback)
     : Snippet(p_name,
               p_description,
+              QString(),
               Snippet::InvalidShortcut,
               false,
               QString(),

+ 9 - 0
src/snippet/snippet.cpp

@@ -16,6 +16,7 @@ Snippet::Snippet(const QString &p_name)
 }
 
 Snippet::Snippet(const QString &p_name,
+                 const QString &p_description,
                  const QString &p_content,
                  int p_shortcut,
                  bool p_indentAsFirstLine,
@@ -23,6 +24,7 @@ Snippet::Snippet(const QString &p_name,
                  const QString &p_selectionMark)
     : m_type(Type::Text),
       m_name(p_name),
+      m_description(p_description),
       m_content(p_content),
       m_shortcut(p_shortcut),
       m_indentAsFirstLine(p_indentAsFirstLine),
@@ -36,6 +38,7 @@ QJsonObject Snippet::toJson() const
     QJsonObject obj;
 
     obj[QStringLiteral("type")] = static_cast<int>(m_type);
+    obj[QStringLiteral("description")] = m_description;
     obj[QStringLiteral("content")] = m_content;
     obj[QStringLiteral("shortcut")] = m_shortcut;
     obj[QStringLiteral("indent_as_first_line")] = m_indentAsFirstLine;
@@ -48,6 +51,7 @@ QJsonObject Snippet::toJson() const
 void Snippet::fromJson(const QJsonObject &p_jobj)
 {
     m_type = static_cast<Type>(p_jobj[QStringLiteral("type")].toInt());
+    m_description = p_jobj[QStringLiteral("description")].toString();
     m_content = p_jobj[QStringLiteral("content")].toString();
     m_shortcut = p_jobj[QStringLiteral("shortcut")].toInt();
     m_indentAsFirstLine = p_jobj[QStringLiteral("indent_as_first_line")].toBool();
@@ -104,6 +108,11 @@ const QString &Snippet::getContent() const
     return m_content;
 }
 
+const QString &Snippet::getDescription() const
+{
+    return m_description;
+}
+
 QString Snippet::apply(const QString &p_selectedText,
                        const QString &p_indentationSpaces,
                        int &p_cursorOffset)

+ 5 - 0
src/snippet/snippet.h

@@ -24,6 +24,7 @@ namespace vnotex
         explicit Snippet(const QString &p_name);
 
         Snippet(const QString &p_name,
+                const QString &p_description,
                 const QString &p_content,
                 int p_shortcut,
                 bool p_indentAsFirstLine,
@@ -43,6 +44,8 @@ namespace vnotex
 
         const QString &getName() const;
 
+        const QString &getDescription() const;
+
         Type getType() const;
 
         int getShortcut() const;
@@ -78,6 +81,8 @@ namespace vnotex
         // To avoid mixed with shortcut, the name should not contain digits.
         QString m_name;
 
+        QString m_description;
+
         // Content of the snippet if it is Text.
         // Embedded snippet is supported.
         QString m_content;

+ 40 - 0
src/utils/iconutils.cpp

@@ -2,6 +2,9 @@
 
 #include <QRegExp>
 #include <QFileInfo>
+#include <QPixmap>
+#include <QPainter>
+#include <QDebug>
 
 #include "fileutils.h"
 
@@ -81,3 +84,40 @@ QIcon IconUtils::fetchIconWithDisabledState(const QString &p_iconFile)
     colors.push_back(OverriddenColor(s_defaultIconDisabledForeground, QIcon::Disabled, QIcon::Off));
     return fetchIcon(p_iconFile, colors);
 }
+
+QIcon IconUtils::drawTextIcon(const QString &p_text,
+                              const QString &p_fg,
+                              const QString &p_border)
+{
+    const int wid = 64;
+    QPixmap pixmap(wid, wid);
+    pixmap.fill(Qt::transparent);
+
+    QPainter painter(&pixmap);
+    painter.setRenderHint(QPainter::Antialiasing);
+
+    auto pen = painter.pen();
+    pen.setColor(p_border);
+    pen.setWidth(3);
+    painter.setPen(pen);
+
+    painter.drawRoundedRect(4, 4, wid - 8, wid - 8, 8, 8);
+
+    if (!p_text.isEmpty()) {
+        pen.setColor(p_fg);
+        painter.setPen(pen);
+
+        auto font = painter.font();
+        font.setPointSize(36);
+        painter.setFont(font);
+
+        auto requriedRect = painter.boundingRect(4, 4, wid - 8, wid - 8,
+                                                 Qt::AlignCenter,
+                                                 p_text);
+        painter.drawText(requriedRect, p_text);
+    }
+
+    QIcon icon;
+    icon.addPixmap(pixmap);
+    return icon;
+}

+ 4 - 0
src/utils/iconutils.h

@@ -46,6 +46,10 @@ namespace vnotex
 
         static QIcon fetchIconWithDisabledState(const QString &p_iconFile);
 
+        static QIcon drawTextIcon(const QString &p_text,
+                                  const QString &p_fg,
+                                  const QString &p_border);
+
     private:
         static QString replaceForegroundOfIcon(const QString &p_iconContent, const QString &p_foreground);
 

+ 26 - 0
src/utils/widgetutils.cpp

@@ -130,6 +130,32 @@ bool WidgetUtils::processKeyEventLikeVi(QWidget *p_widget,
         break;
     }
 
+    case Qt::Key_H:
+    {
+        if (isViControlModifier(modifiers)) {
+            auto upEvent = new QKeyEvent(QEvent::KeyPress,
+                                         Qt::Key_Left,
+                                         Qt::NoModifier);
+            QCoreApplication::postEvent(p_widget, upEvent);
+            eventHandled = true;
+        }
+
+        break;
+    }
+
+    case Qt::Key_L:
+    {
+        if (isViControlModifier(modifiers)) {
+            auto upEvent = new QKeyEvent(QEvent::KeyPress,
+                                         Qt::Key_Right,
+                                         Qt::NoModifier);
+            QCoreApplication::postEvent(p_widget, upEvent);
+            eventHandled = true;
+        }
+
+        break;
+    }
+
     default:
         break;
     }

+ 4 - 4
src/widgets/dialogs/deleteconfirmdialog.cpp

@@ -20,12 +20,12 @@ DeleteConfirmDialog::DeleteConfirmDialog(const QString &p_title,
                                          const QString &p_info,
                                          const QVector<ConfirmItemInfo> &p_items,
                                          DeleteConfirmDialog::Flags p_flags,
-                                         bool p_noAskChecked,
+                                         bool p_noAskAgainChecked,
                                          QWidget *p_parent)
     : ScrollDialog(p_parent),
       m_items(p_items)
 {
-    setupUI(p_title, p_text, p_info, p_flags, p_noAskChecked);
+    setupUI(p_title, p_text, p_info, p_flags, p_noAskAgainChecked);
 
     updateItemsList();
 
@@ -36,7 +36,7 @@ void DeleteConfirmDialog::setupUI(const QString &p_title,
                                   const QString &p_text,
                                   const QString &p_info,
                                   DeleteConfirmDialog::Flags p_flags,
-                                  bool p_noAskChecked)
+                                  bool p_noAskAgainChecked)
 {
     auto mainWidget = new QWidget(this);
     setCentralWidget(mainWidget);
@@ -60,7 +60,7 @@ void DeleteConfirmDialog::setupUI(const QString &p_title,
     // Ask again.
     if (p_flags & Flag::AskAgain) {
         m_noAskCB = new QCheckBox(tr("Do not ask again"), mainWidget);
-        m_noAskCB->setChecked(p_noAskChecked);
+        m_noAskCB->setChecked(p_noAskAgainChecked);
         mainLayout->addWidget(m_noAskCB);
     }
 

+ 7 - 7
src/widgets/dialogs/deleteconfirmdialog.h

@@ -66,12 +66,12 @@ namespace vnotex
         Q_DECLARE_FLAGS(Flags, Flag)
 
         DeleteConfirmDialog(const QString &p_title,
-                           const QString &p_text,
-                           const QString &p_info,
-                           const QVector<ConfirmItemInfo> &p_items,
-                           DeleteConfirmDialog::Flags p_flags,
-                           bool p_noAskChecked,
-                           QWidget *p_parent = nullptr);
+                            const QString &p_text,
+                            const QString &p_info,
+                            const QVector<ConfirmItemInfo> &p_items,
+                            DeleteConfirmDialog::Flags p_flags,
+                            bool p_noAskAgainChecked,
+                            QWidget *p_parent = nullptr);
 
         QVector<ConfirmItemInfo> getConfirmedItems() const;
 
@@ -87,7 +87,7 @@ namespace vnotex
                      const QString &p_text,
                      const QString &p_info,
                      DeleteConfirmDialog::Flags p_flags,
-                     bool p_noAskChecked);
+                     bool p_noAskAgainChecked);
 
         void updateItemsList();
 

+ 1 - 0
src/widgets/dialogs/newsnippetdialog.cpp

@@ -51,6 +51,7 @@ void NewSnippetDialog::acceptedButtonClicked()
 bool NewSnippetDialog::newSnippet()
 {
     auto snip = QSharedPointer<Snippet>::create(m_infoWidget->getName(),
+                                                m_infoWidget->getDescription(),
                                                 m_infoWidget->getContent(),
                                                 m_infoWidget->getShortcut(),
                                                 m_infoWidget->shouldIndentAsFirstLine(),

+ 21 - 0
src/widgets/dialogs/snippetinfowidget.cpp

@@ -44,6 +44,11 @@ void SnippetInfoWidget::setupUI()
 
     setFocusProxy(m_nameLineEdit);
 
+    m_descriptionLineEdit = WidgetsFactory::createLineEdit(this);
+    connect(m_descriptionLineEdit, &QLineEdit::textEdited,
+            this, &SnippetInfoWidget::inputEdited);
+    mainLayout->addRow(tr("Description:"), m_descriptionLineEdit);
+
     setupTypeComboBox(this);
     mainLayout->addRow(tr("Type:"), m_typeComboBox);
 
@@ -134,6 +139,11 @@ QString SnippetInfoWidget::getContent() const
     return m_contentTextEdit->toPlainText();
 }
 
+QString SnippetInfoWidget::getDescription() const
+{
+    return m_descriptionLineEdit->text();
+}
+
 void SnippetInfoWidget::setSnippet(const Snippet *p_snippet)
 {
     if (m_snippet == p_snippet) {
@@ -144,15 +154,26 @@ void SnippetInfoWidget::setSnippet(const Snippet *p_snippet)
     m_snippet = p_snippet;
     initShortcutComboBox();
     if (m_snippet) {
+        const bool readOnly = m_snippet->isReadOnly();
         m_nameLineEdit->setText(m_snippet->getName());
+        m_nameLineEdit->setEnabled(!readOnly);
+        m_descriptionLineEdit->setText(m_snippet->getDescription());
+        m_descriptionLineEdit->setEnabled(!readOnly);
         m_typeComboBox->setCurrentIndex(m_typeComboBox->findData(static_cast<int>(m_snippet->getType())));
+        m_typeComboBox->setEnabled(!readOnly);
         m_shortcutComboBox->setCurrentIndex(m_shortcutComboBox->findData(m_snippet->getShortcut()));
+        m_shortcutComboBox->setEnabled(!readOnly);
         m_cursorMarkLineEdit->setText(m_snippet->getCursorMark());
+        m_cursorMarkLineEdit->setEnabled(!readOnly);
         m_selectionMarkLineEdit->setText(m_snippet->getSelectionMark());
+        m_selectionMarkLineEdit->setEnabled(!readOnly);
         m_indentAsFirstLineCheckBox->setChecked(m_snippet->isIndentAsFirstLineEnabled());
+        m_indentAsFirstLineCheckBox->setEnabled(!readOnly);
         m_contentTextEdit->setPlainText(m_snippet->getContent());
+        m_contentTextEdit->setEnabled(!readOnly);
     } else {
         m_nameLineEdit->clear();
+        m_descriptionLineEdit->clear();
         m_typeComboBox->setCurrentIndex(m_typeComboBox->findData(static_cast<int>(Snippet::Type::Text)));
         m_shortcutComboBox->setCurrentIndex(m_shortcutComboBox->findData(Snippet::InvalidShortcut));
         m_cursorMarkLineEdit->setText(Snippet::c_defaultCursorMark);

+ 4 - 0
src/widgets/dialogs/snippetinfowidget.h

@@ -36,6 +36,8 @@ namespace vnotex
 
         QString getContent() const;
 
+        QString getDescription() const;
+
     signals:
         void inputEdited();
 
@@ -56,6 +58,8 @@ namespace vnotex
 
         QLineEdit *m_nameLineEdit = nullptr;
 
+        QLineEdit *m_descriptionLineEdit = nullptr;
+
         QComboBox *m_typeComboBox = nullptr;
 
         QComboBox *m_shortcutComboBox = nullptr;

+ 1 - 0
src/widgets/dialogs/snippetpropertiesdialog.cpp

@@ -79,6 +79,7 @@ void SnippetPropertiesDialog::acceptedButtonClicked()
 bool SnippetPropertiesDialog::saveSnippetProperties()
 {
     auto snip = QSharedPointer<Snippet>::create(m_infoWidget->getName(),
+                                                m_infoWidget->getDescription(),
                                                 m_infoWidget->getContent(),
                                                 m_infoWidget->getShortcut(),
                                                 m_infoWidget->shouldIndentAsFirstLine(),

+ 32 - 0
src/widgets/floatingwidget.cpp

@@ -0,0 +1,32 @@
+#include "floatingwidget.h"
+
+#include <QMenu>
+
+using namespace vnotex;
+
+FloatingWidget::FloatingWidget(QWidget *p_parent)
+    : QWidget(p_parent)
+{
+}
+
+void FloatingWidget::showEvent(QShowEvent *p_event)
+{
+    QWidget::showEvent(p_event);
+
+    // May fix potential input method issue.
+    activateWindow();
+
+    setFocus();
+}
+
+void FloatingWidget::finish()
+{
+    if (m_menu) {
+        m_menu->hide();
+    }
+}
+
+void FloatingWidget::setMenu(QMenu *p_menu)
+{
+    m_menu = p_menu;
+}

+ 33 - 0
src/widgets/floatingwidget.h

@@ -0,0 +1,33 @@
+#ifndef FLOATINGWIDGET_H
+#define FLOATINGWIDGET_H
+
+#include <QWidget>
+#include <QVariant>
+
+class QMenu;
+
+namespace vnotex
+{
+    // Used for ViewWindow to show as a floating widget (usually via QMenu).
+    class FloatingWidget : public QWidget
+    {
+        Q_OBJECT
+    public:
+        void setMenu(QMenu *p_menu);
+
+        virtual QVariant result() const = 0;
+
+    protected:
+        FloatingWidget(QWidget *p_parent = nullptr);
+
+        // Sub-class calls this to indicates completion.
+        void finish();
+
+        void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE;
+
+    private:
+        QMenu *m_menu = nullptr;
+    };
+}
+
+#endif // FLOATINGWIDGET_H

+ 18 - 7
src/widgets/markdownviewwindow.cpp

@@ -31,7 +31,6 @@
 #include "editors/statuswidget.h"
 #include "editors/plantumlhelper.h"
 #include "editors/graphvizhelper.h"
-#include <snippet/snippetmgr.h>
 
 using namespace vnotex;
 
@@ -979,13 +978,25 @@ void MarkdownViewWindow::setupPreviewHelper()
 
 void MarkdownViewWindow::applySnippet(const QString &p_name)
 {
-    if (isReadMode() || m_editor->isReadOnly()) {
-        qWarning() << "failed to apply snippet in read mode or to a read-only buffer" << p_name;
+    if (isReadMode()) {
+        qWarning() << "failed to apply snippet in read mode" << p_name;
         return;
     }
 
-    m_editor->enterInsertModeIfApplicable();
-    SnippetMgr::getInst().applySnippet(p_name,
-                                       m_editor->getTextEdit(),
-                                       SnippetMgr::generateOverrides(getBuffer()));
+    TextViewWindowHelper::applySnippet(this, p_name);
+}
+
+void MarkdownViewWindow::applySnippet()
+{
+    if (isReadMode()) {
+        qWarning() << "failed to apply snippet in read mode";
+        return;
+    }
+
+    TextViewWindowHelper::applySnippet(this);
+}
+
+QPoint MarkdownViewWindow::getFloatingWidgetPosition()
+{
+    return TextViewWindowHelper::getFloatingWidgetPosition(this);
 }

+ 4 - 0
src/widgets/markdownviewwindow.h

@@ -46,6 +46,8 @@ namespace vnotex
 
         void applySnippet(const QString &p_name) Q_DECL_OVERRIDE;
 
+        void applySnippet() Q_DECL_OVERRIDE;
+
     public slots:
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
 
@@ -85,6 +87,8 @@ namespace vnotex
 
         void zoom(bool p_zoomIn) Q_DECL_OVERRIDE;
 
+        QPoint getFloatingWidgetPosition() Q_DECL_OVERRIDE;
+
     private:
         void setupUI();
 

+ 233 - 0
src/widgets/quickselector.cpp

@@ -0,0 +1,233 @@
+#include "quickselector.h"
+
+#include <QVBoxLayout>
+#include <QWidgetAction>
+#include <QMenu>
+#include <QLabel>
+#include <QListWidgetItem>
+#include <QKeyEvent>
+#include <QDebug>
+#include <QRegularExpression>
+
+#include <utils/widgetutils.h>
+#include <utils/iconutils.h>
+
+#include "lineedit.h"
+#include "listwidget.h"
+#include "widgetsfactory.h"
+
+using namespace vnotex;
+
+QuickSelectorItem::QuickSelectorItem(const QVariant &p_key,
+                                     const QString &p_name,
+                                     const QString &p_tip,
+                                     const QString &p_shortcut)
+    : m_key(p_key),
+      m_name(p_name),
+      m_tip(p_tip),
+      m_shortcut(p_shortcut)
+{
+    Q_ASSERT(m_shortcut.size() < 3);
+}
+
+static bool selectorItemCmp(const QuickSelectorItem &p_a, const QuickSelectorItem &p_b)
+{
+    if (p_a.m_shortcut.isEmpty()) {
+        if (p_b.m_shortcut.isEmpty()) {
+            return p_a.m_name < p_b.m_name;
+        }
+        return false;
+    } else {
+        if (p_b.m_shortcut.isEmpty()) {
+            return true;
+        }
+
+        return p_a.m_shortcut < p_b.m_shortcut;
+    }
+}
+
+QuickSelector::QuickSelector(const QString &p_title,
+                             const QVector<QuickSelectorItem> &p_items,
+                             bool p_sortByShortcut,
+                             QWidget *p_parent)
+    : FloatingWidget(p_parent),
+      m_items(p_items)
+{
+    if (p_sortByShortcut) {
+        std::sort(m_items.begin(), m_items.end(), selectorItemCmp);
+    }
+
+    setupUI(p_title);
+
+    updateItemList();
+}
+
+void QuickSelector::setupUI(const QString &p_title)
+{
+    auto mainLayout = new QVBoxLayout(this);
+
+    if (!p_title.isEmpty()) {
+        mainLayout->addWidget(new QLabel(p_title, this));
+    }
+
+    m_searchLineEdit = WidgetsFactory::createLineEdit(this);
+    connect(m_searchLineEdit, &QLineEdit::textEdited,
+            this, &QuickSelector::searchAndFilter);
+    mainLayout->addWidget(m_searchLineEdit);
+
+    setFocusProxy(m_searchLineEdit);
+    m_searchLineEdit->installEventFilter(this);
+
+    m_itemList = new ListWidget(this);
+    m_itemList->setWrapping(true);
+    m_itemList->setFlow(QListView::LeftToRight);
+    m_itemList->setIconSize(QSize(18, 18));
+    connect(m_itemList, &QListWidget::itemActivated,
+            this, &QuickSelector::activateItem);
+    mainLayout->addWidget(m_itemList);
+
+    m_itemList->installEventFilter(this);
+}
+
+void QuickSelector::updateItemList()
+{
+    m_itemList->clear();
+
+    for (int i = 0; i < m_items.size(); ++i) {
+        const auto &item = m_items[i];
+
+        auto listItem = new QListWidgetItem(m_itemList);
+        auto icon = IconUtils::drawTextIcon(item.m_shortcut, "blue", "darkgreen");
+        listItem->setIcon(icon);
+
+        listItem->setText(item.m_name);
+        listItem->setToolTip(item.m_tip);
+        listItem->setData(Qt::UserRole, i);
+    }
+
+    Q_ASSERT(!m_items.isEmpty());
+    m_itemList->setCurrentRow(0);
+}
+
+void QuickSelector::activateItem(const QListWidgetItem *p_item)
+{
+    if (p_item) {
+        m_selectedKey = getSelectorItem(p_item).m_key;
+    }
+    finish();
+}
+
+void QuickSelector::activate(const QuickSelectorItem *p_item)
+{
+    m_selectedKey = p_item->m_key;
+    finish();
+}
+
+QuickSelectorItem &QuickSelector::getSelectorItem(const QListWidgetItem *p_item)
+{
+    Q_ASSERT(p_item);
+    return m_items[p_item->data(Qt::UserRole).toInt()];
+}
+
+QVariant QuickSelector::result() const
+{
+    return m_selectedKey;
+}
+
+bool QuickSelector::eventFilter(QObject *p_obj, QEvent *p_event)
+{
+    if ((p_obj == m_searchLineEdit || p_obj == m_itemList)
+        && p_event->type() == QEvent::KeyPress) {
+        auto keyEve = static_cast<QKeyEvent *>(p_event);
+        const auto key = keyEve->key();
+        if (key == Qt::Key_Tab || key == Qt::Key_Backtab) {
+            // Change focus.
+            if (p_obj == m_searchLineEdit) {
+                m_itemList->setFocus();
+            } else {
+                m_searchLineEdit->setFocus();
+            }
+            return true;
+        } else if (key == Qt::Key_Enter || key == Qt::Key_Return) {
+            if (p_obj == m_searchLineEdit) {
+                activateItem(m_itemList->currentItem());
+                return true;
+            }
+        }
+    }
+    return FloatingWidget::eventFilter(p_obj, p_event);
+}
+
+void QuickSelector::searchAndFilter(const QString &p_text)
+{
+    auto text = p_text.trimmed();
+    if (text.isEmpty()) {
+        // Show all items.
+        filterItems([](const QuickSelectorItem &) {
+                return true;
+            });
+        return;
+    } else if (text.size() < 3) {
+        // Check shortcut first.
+        const QuickSelectorItem *hitItem = nullptr;
+        int ret = filterItems([&text, &hitItem](const QuickSelectorItem &p_item) {
+                if (p_item.m_shortcut == text) {
+                    hitItem = &p_item;
+                    return true;
+                } else if (p_item.m_shortcut.startsWith(text)) {
+                    return true;
+                }
+                return false;
+            });
+
+        if (hitItem) {
+            activate(hitItem);
+            return;
+        }
+
+        if (ret > 0) {
+            return;
+        }
+    }
+
+    // Check name.
+    auto parts = text.split(QLatin1Char(' '), QString::SkipEmptyParts);
+    Q_ASSERT(!parts.isEmpty());
+    QRegularExpression regExp;
+    regExp.setPatternOptions(regExp.patternOptions() | QRegularExpression::CaseInsensitiveOption);
+    if (parts.size() == 1) {
+        regExp.setPattern(QRegularExpression::escape(parts[0]));
+    } else {
+        QString pattern = QRegularExpression::escape(parts[0]);
+        for (int i = 1; i < parts.size(); ++i) {
+            pattern += ".*" + QRegularExpression::escape(parts[i]);
+        }
+        regExp.setPattern(pattern);
+    }
+    filterItems([&regExp](const QuickSelectorItem &p_item) {
+            if (p_item.m_name.indexOf(regExp) != -1) {
+                return true;
+            }
+            return false;
+        });
+}
+
+int QuickSelector::filterItems(const std::function<bool(const QuickSelectorItem &)> &p_judge)
+{
+    const int cnt = m_itemList->count();
+    int matchedCnt = 0;
+    int firstHit = -1;
+    for (int i = 0; i < cnt; ++i) {
+        auto item = m_itemList->item(i);
+        bool hit = p_judge(getSelectorItem(item));
+        if (hit) {
+            if (matchedCnt == 0) {
+                firstHit = i;
+            }
+            ++matchedCnt;
+        }
+        item->setHidden(!hit);
+    }
+    m_itemList->setCurrentRow(firstHit);
+    return matchedCnt;
+}

+ 74 - 0
src/widgets/quickselector.h

@@ -0,0 +1,74 @@
+#ifndef QUICKSELECTOR_H
+#define QUICKSELECTOR_H
+
+#include "floatingwidget.h"
+
+#include <QVariant>
+#include <QVector>
+
+class QLineEdit;
+class QListWidget;
+class QListWidgetItem;
+
+namespace vnotex
+{
+    struct QuickSelectorItem
+    {
+        QuickSelectorItem() = default;
+
+        QuickSelectorItem(const QVariant &p_key,
+                          const QString &p_name,
+                          const QString &p_tip,
+                          const QString &p_shortcut);
+
+        QVariant m_key;
+
+        QString m_name;
+
+        QString m_tip;
+
+        // Empty or size < 3.
+        QString m_shortcut;
+    };
+
+    class QuickSelector : public FloatingWidget
+    {
+        Q_OBJECT
+    public:
+        QuickSelector(const QString &p_title,
+                      const QVector<QuickSelectorItem> &p_items,
+                      bool p_sortByShortcut,
+                      QWidget *p_parent = nullptr);
+
+        QVariant result() const Q_DECL_OVERRIDE;
+
+    protected:
+        bool eventFilter(QObject *p_obj, QEvent *p_event) Q_DECL_OVERRIDE;
+
+    private:
+        void setupUI(const QString &p_title);
+
+        void updateItemList();
+
+        void activateItem(const QListWidgetItem *p_item);
+
+        void activate(const QuickSelectorItem *p_item);
+
+        void searchAndFilter(const QString &p_text);
+
+        // Return the number of items that hit @p_judge.
+        int filterItems(const std::function<bool(const QuickSelectorItem &)> &p_judge);
+
+        QuickSelectorItem &getSelectorItem(const QListWidgetItem *p_item);
+
+        QVector<QuickSelectorItem> m_items;
+
+        QLineEdit *m_searchLineEdit = nullptr;
+
+        QListWidget *m_itemList = nullptr;
+
+        QVariant m_selectedKey;
+    };
+}
+
+#endif // QUICKSELECTOR_H

+ 1 - 0
src/widgets/snippetpanel.cpp

@@ -103,6 +103,7 @@ void SnippetPanel::updateSnippetList()
         }
 
         item->setData(Qt::UserRole, snippet->getName());
+        item->setToolTip(snippet->getDescription());
     }
 
     updateItemsCountLabel();

+ 10 - 9
src/widgets/textviewwindow.cpp

@@ -15,7 +15,6 @@
 #include <core/thememgr.h>
 #include "editors/statuswidget.h"
 #include <core/fileopenparameters.h>
-#include <snippet/snippetmgr.h>
 
 using namespace vnotex;
 
@@ -257,13 +256,15 @@ ViewWindowSession TextViewWindow::saveSession() const
 
 void TextViewWindow::applySnippet(const QString &p_name)
 {
-    if (m_editor->isReadOnly()) {
-        qWarning() << "failed to apply snippet to a read-only buffer" << p_name;
-        return;
-    }
+    TextViewWindowHelper::applySnippet(this, p_name);
+}
 
-    m_editor->enterInsertModeIfApplicable();
-    SnippetMgr::getInst().applySnippet(p_name,
-                                       m_editor->getTextEdit(),
-                                       SnippetMgr::generateOverrides(getBuffer()));
+void TextViewWindow::applySnippet()
+{
+    TextViewWindowHelper::applySnippet(this);
+}
+
+QPoint TextViewWindow::getFloatingWidgetPosition()
+{
+    return TextViewWindowHelper::getFloatingWidgetPosition(this);
 }

+ 4 - 0
src/widgets/textviewwindow.h

@@ -33,6 +33,8 @@ namespace vnotex
 
         void applySnippet(const QString &p_name) Q_DECL_OVERRIDE;
 
+        void applySnippet() Q_DECL_OVERRIDE;
+
     public slots:
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
 
@@ -62,6 +64,8 @@ namespace vnotex
 
         void zoom(bool p_zoomIn) Q_DECL_OVERRIDE;
 
+        QPoint getFloatingWidgetPosition() Q_DECL_OVERRIDE;
+
     private:
         void setupUI();
 

+ 104 - 0
src/widgets/textviewwindowhelper.h

@@ -2,11 +2,17 @@
 #define TEXTVIEWWINDOWHELPER_H
 
 #include <QFileInfo>
+#include <QTextCursor>
+#include <QRegularExpression>
+#include <QTextBlock>
 
 #include <vtextedit/texteditorconfig.h>
 #include <core/texteditorconfig.h>
 #include <core/configmgr.h>
 #include <utils/widgetutils.h>
+#include <snippet/snippetmgr.h>
+
+#include "quickselector.h"
 
 namespace vnotex
 {
@@ -181,6 +187,104 @@ namespace vnotex
             p_win->m_editor->clearIncrementalSearchHighlight();
             p_win->m_editor->clearSearchHighlight();
         }
+
+        template <typename _ViewWindow>
+        static void applySnippet(_ViewWindow *p_win, const QString &p_name)
+        {
+            if (p_win->m_editor->isReadOnly() || p_name.isEmpty()) {
+                qWarning() << "failed to apply snippet" << p_name << "to a read-only buffer";
+                return;
+            }
+
+            SnippetMgr::getInst().applySnippet(p_name,
+                                               p_win->m_editor->getTextEdit(),
+                                               SnippetMgr::generateOverrides(p_win->getBuffer()));
+            p_win->m_editor->enterInsertModeIfApplicable();
+            p_win->showMessage(ViewWindow::tr("Snippet applied: %1").arg(p_name));
+        }
+
+        template <typename _ViewWindow>
+        static void applySnippet(_ViewWindow *p_win)
+        {
+            if (p_win->m_editor->isReadOnly()) {
+                qWarning() << "failed to apply snippet to a read-only buffer";
+                return;
+            }
+
+            QString snippetName;
+
+            auto textEdit = p_win->m_editor->getTextEdit();
+            if (!textEdit->hasSelection()) {
+                // Fetch the snippet symbol containing current cursor.
+                auto cursor = textEdit->textCursor();
+                const auto block = cursor.block();
+                const auto text = block.text();
+                const int pib = cursor.positionInBlock();
+                QRegularExpression regExp(SnippetMgr::c_snippetSymbolRegExp);
+                QRegularExpressionMatch match;
+                int idx = text.lastIndexOf(regExp, pib, &match);
+                if (idx >= 0 && (idx + match.capturedLength(0) >= pib)) {
+                    // Found one symbol under current cursor.
+                    snippetName = match.captured(1);
+                    if (!SnippetMgr::getInst().find(snippetName)) {
+                        p_win->showMessage(ViewWindow::tr("Snippet (%1) not found").arg(snippetName));
+                        return;
+                    }
+
+                    // Remove the symbol and apply snippet later.
+                    cursor.setPosition(block.position() + idx);
+                    cursor.setPosition(block.position() + idx + match.capturedLength(0), QTextCursor::KeepAnchor);
+                    cursor.removeSelectedText();
+                    textEdit->setTextCursor(cursor);
+                }
+            }
+
+            if (snippetName.isEmpty()) {
+                // Prompt for snippet.
+                snippetName = promptForSnippet(p_win);
+            }
+
+            if (!snippetName.isEmpty()) {
+                applySnippet(p_win, snippetName);
+            }
+        }
+
+        template <typename _ViewWindow>
+        static QString promptForSnippet(_ViewWindow *p_win)
+        {
+            const auto snippets = SnippetMgr::getInst().getSnippets();
+            if (snippets.isEmpty()) {
+                p_win->showMessage(ViewWindow::tr("Snippet not available"));
+                return QString();
+            }
+
+            QVector<QuickSelectorItem> items;
+            for (const auto &snip : snippets) {
+                items.push_back(QuickSelectorItem(snip->getName(),
+                                                  snip->getName(),
+                                                  snip->getDescription(),
+                                                  snip->getShortcutString()));
+            }
+
+            // Ownership will be transferred to showFloatingWidget().
+            auto selector = new QuickSelector(ViewWindow::tr("Select Snippet"),
+                                              items,
+                                              true,
+                                              p_win);
+            auto ret = p_win->showFloatingWidget(selector);
+            return ret.toString();
+        }
+
+        template <typename _ViewWindow>
+        static QPoint getFloatingWidgetPosition(_ViewWindow *p_win)
+        {
+            auto textEdit = p_win->m_editor->getTextEdit();
+            auto localPos = textEdit->cursorRect().bottomRight();
+            if (!textEdit->rect().contains(localPos)) {
+                localPos = QPoint(5, 5);
+            }
+            return textEdit->mapToGlobal(localPos);
+        }
     };
 }
 

+ 34 - 0
src/widgets/viewwindow.cpp

@@ -13,6 +13,7 @@
 #include <QFocusEvent>
 #include <QShortcut>
 #include <QWheelEvent>
+#include <QWidgetAction>
 
 #include <core/fileopenparameters.h>
 #include "toolbarhelper.h"
@@ -33,6 +34,7 @@
 #include "findandreplacewidget.h"
 #include "editors/statuswidget.h"
 #include "propertydefs.h"
+#include "floatingwidget.h"
 
 using namespace vnotex;
 
@@ -865,6 +867,17 @@ void ViewWindow::setupShortcuts()
                     });
         }
     }
+
+    // ApplySnippet.
+    {
+        auto shortcut = WidgetUtils::createShortcut(editorConfig.getShortcut(EditorConfig::ApplySnippet), this, Qt::WidgetWithChildrenShortcut);
+        if (shortcut) {
+            connect(shortcut, &QShortcut::activated,
+                    this, [this]() {
+                        applySnippet();
+                    });
+        }
+    }
 }
 
 void ViewWindow::wheelEvent(QWheelEvent *p_event)
@@ -1108,3 +1121,24 @@ void ViewWindow::setWindowFlags(WindowFlags p_flags)
 {
     m_flags = p_flags;
 }
+
+QVariant ViewWindow::showFloatingWidget(FloatingWidget *p_widget)
+{
+    // Show the widget through a QWidgetAction in menu.
+    QMenu menu;
+
+    auto act = new QWidgetAction(&menu);
+    // @act will own @p_widget.
+    act->setDefaultWidget(p_widget);
+    menu.addAction(act);
+
+    p_widget->setMenu(&menu);
+
+    menu.exec(getFloatingWidgetPosition());
+    return p_widget->result();
+}
+
+QPoint ViewWindow::getFloatingWidgetPosition()
+{
+    return mapToGlobal(QPoint(5, 5));
+}

+ 12 - 3
src/widgets/viewwindow.h

@@ -25,6 +25,7 @@ namespace vnotex
     class EditReadDiscardAction;
     class FindAndReplaceWidget;
     class StatusWidget;
+    class FloatingWidget;
 
     class ViewWindow : public QFrame
     {
@@ -86,6 +87,12 @@ namespace vnotex
 
         virtual void applySnippet(const QString &p_name) = 0;
 
+        virtual void applySnippet() = 0;
+
+        // Take ownership of @p_widget.
+        // Return the result from the FloatingWidget.
+        QVariant showFloatingWidget(FloatingWidget *p_widget);
+
     public slots:
         virtual void handleEditorConfigChange() = 0;
 
@@ -163,9 +170,6 @@ namespace vnotex
 
         virtual void handleFindAndReplaceWidgetOpened();
 
-        // Show message in status widget if exists. Otherwise, show it in the mainwindow's status widget.
-        void showMessage(const QString p_msg);
-
     protected:
         void setCentralWidget(QWidget *p_widget);
 
@@ -228,6 +232,11 @@ namespace vnotex
 
         void read(bool p_save);
 
+        // Show message in status widget if exists. Otherwise, show it in the mainwindow's status widget.
+        void showMessage(const QString p_msg);
+
+        virtual QPoint getFloatingWidgetPosition();
+
         static QToolBar *createToolBar(QWidget *p_parent = nullptr);
 
         // The revision of the buffer of the last sync content.

+ 4 - 0
src/widgets/widgets.pri

@@ -47,6 +47,7 @@ SOURCES += \
     $$PWD/filesystemviewer.cpp \
     $$PWD/dialogs/folderfilesfilterwidget.cpp \
     $$PWD/findandreplacewidget.cpp \
+    $$PWD/floatingwidget.cpp \
     $$PWD/fullscreentoggleaction.cpp \
     $$PWD/lineedit.cpp \
     $$PWD/lineeditdelegate.cpp \
@@ -62,6 +63,7 @@ SOURCES += \
     $$PWD/outlineprovider.cpp \
     $$PWD/outlineviewer.cpp \
     $$PWD/propertydefs.cpp \
+    $$PWD/quickselector.cpp \
     $$PWD/searchinfoprovider.cpp \
     $$PWD/searchpanel.cpp \
     $$PWD/snippetpanel.cpp \
@@ -151,6 +153,7 @@ HEADERS += \
     $$PWD/filesystemviewer.h \
     $$PWD/dialogs/folderfilesfilterwidget.h \
     $$PWD/findandreplacewidget.h \
+    $$PWD/floatingwidget.h \
     $$PWD/fullscreentoggleaction.h \
     $$PWD/lineedit.h \
     $$PWD/lineeditdelegate.h \
@@ -167,6 +170,7 @@ HEADERS += \
     $$PWD/outlineprovider.h \
     $$PWD/outlineviewer.h \
     $$PWD/propertydefs.h \
+    $$PWD/quickselector.h \
     $$PWD/searchinfoprovider.h \
     $$PWD/searchpanel.h \
     $$PWD/snippetpanel.h \