From 58e8ea5ee81905b316e2c6e29259e6380c86e238 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Thu, 8 Jul 2021 21:31:13 +0800 Subject: [PATCH] improve Snippet support --- libs/vtextedit | 2 +- src/core/editorconfig.h | 1 + src/core/mainconfig.cpp | 1 + src/data/core/vnotex.json | 7 +- src/snippet/dynamicsnippet.cpp | 1 + src/snippet/snippet.cpp | 9 + src/snippet/snippet.h | 5 + src/utils/iconutils.cpp | 40 +++ src/utils/iconutils.h | 4 + src/utils/widgetutils.cpp | 26 ++ src/widgets/dialogs/deleteconfirmdialog.cpp | 8 +- src/widgets/dialogs/deleteconfirmdialog.h | 14 +- src/widgets/dialogs/newsnippetdialog.cpp | 1 + src/widgets/dialogs/snippetinfowidget.cpp | 21 ++ src/widgets/dialogs/snippetinfowidget.h | 4 + .../dialogs/snippetpropertiesdialog.cpp | 1 + src/widgets/floatingwidget.cpp | 32 +++ src/widgets/floatingwidget.h | 33 +++ src/widgets/markdownviewwindow.cpp | 25 +- src/widgets/markdownviewwindow.h | 4 + src/widgets/quickselector.cpp | 233 ++++++++++++++++++ src/widgets/quickselector.h | 74 ++++++ src/widgets/snippetpanel.cpp | 1 + src/widgets/textviewwindow.cpp | 21 +- src/widgets/textviewwindow.h | 4 + src/widgets/textviewwindowhelper.h | 104 ++++++++ src/widgets/viewwindow.cpp | 34 +++ src/widgets/viewwindow.h | 15 +- src/widgets/widgets.pri | 4 + 29 files changed, 694 insertions(+), 35 deletions(-) create mode 100644 src/widgets/floatingwidget.cpp create mode 100644 src/widgets/floatingwidget.h create mode 100644 src/widgets/quickselector.cpp create mode 100644 src/widgets/quickselector.h diff --git a/libs/vtextedit b/libs/vtextedit index 98274148..7045758b 160000 --- a/libs/vtextedit +++ b/libs/vtextedit @@ -1 +1 @@ -Subproject commit 98274148a0e1ad371f29abe072fac35bf5d7b6df +Subproject commit 7045758b2c9c10f6b72b97f15c40ded97db6ac0d diff --git a/src/core/editorconfig.h b/src/core/editorconfig.h index 28777d14..96c0578e 100644 --- a/src/core/editorconfig.h +++ b/src/core/editorconfig.h @@ -49,6 +49,7 @@ namespace vnotex FindAndReplace, FindNext, FindPrevious, + ApplySnippet, MaxShortcut }; Q_ENUM(Shortcut) diff --git a/src/core/mainconfig.cpp b/src/core/mainconfig.cpp index 3bfcabc3..c9151277 100644 --- a/src/core/mainconfig.cpp +++ b/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(); } diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index 1541e0a1..7c3aa83c 100644 --- a/src/data/core/vnotex.json +++ b/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" diff --git a/src/snippet/dynamicsnippet.cpp b/src/snippet/dynamicsnippet.cpp index 7429bff7..029fe0fc 100644 --- a/src/snippet/dynamicsnippet.cpp +++ b/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(), diff --git a/src/snippet/snippet.cpp b/src/snippet/snippet.cpp index 37232e97..5c2c3e5e 100644 --- a/src/snippet/snippet.cpp +++ b/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(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(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) diff --git a/src/snippet/snippet.h b/src/snippet/snippet.h index 99fc0213..5310436e 100644 --- a/src/snippet/snippet.h +++ b/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; diff --git a/src/utils/iconutils.cpp b/src/utils/iconutils.cpp index bb227a59..82256eba 100644 --- a/src/utils/iconutils.cpp +++ b/src/utils/iconutils.cpp @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #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; +} diff --git a/src/utils/iconutils.h b/src/utils/iconutils.h index 6de4036f..ef4e93ed 100644 --- a/src/utils/iconutils.h +++ b/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); diff --git a/src/utils/widgetutils.cpp b/src/utils/widgetutils.cpp index 4e07c9db..9e219ec7 100644 --- a/src/utils/widgetutils.cpp +++ b/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; } diff --git a/src/widgets/dialogs/deleteconfirmdialog.cpp b/src/widgets/dialogs/deleteconfirmdialog.cpp index 026882a2..d45170cd 100644 --- a/src/widgets/dialogs/deleteconfirmdialog.cpp +++ b/src/widgets/dialogs/deleteconfirmdialog.cpp @@ -20,12 +20,12 @@ DeleteConfirmDialog::DeleteConfirmDialog(const QString &p_title, const QString &p_info, const QVector &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); } diff --git a/src/widgets/dialogs/deleteconfirmdialog.h b/src/widgets/dialogs/deleteconfirmdialog.h index e3445604..e3483363 100644 --- a/src/widgets/dialogs/deleteconfirmdialog.h +++ b/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 &p_items, - DeleteConfirmDialog::Flags p_flags, - bool p_noAskChecked, - QWidget *p_parent = nullptr); + const QString &p_text, + const QString &p_info, + const QVector &p_items, + DeleteConfirmDialog::Flags p_flags, + bool p_noAskAgainChecked, + QWidget *p_parent = nullptr); QVector 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(); diff --git a/src/widgets/dialogs/newsnippetdialog.cpp b/src/widgets/dialogs/newsnippetdialog.cpp index e0cbc166..e95f85b5 100644 --- a/src/widgets/dialogs/newsnippetdialog.cpp +++ b/src/widgets/dialogs/newsnippetdialog.cpp @@ -51,6 +51,7 @@ void NewSnippetDialog::acceptedButtonClicked() bool NewSnippetDialog::newSnippet() { auto snip = QSharedPointer::create(m_infoWidget->getName(), + m_infoWidget->getDescription(), m_infoWidget->getContent(), m_infoWidget->getShortcut(), m_infoWidget->shouldIndentAsFirstLine(), diff --git a/src/widgets/dialogs/snippetinfowidget.cpp b/src/widgets/dialogs/snippetinfowidget.cpp index 92146e34..9192b5af 100644 --- a/src/widgets/dialogs/snippetinfowidget.cpp +++ b/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(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(Snippet::Type::Text))); m_shortcutComboBox->setCurrentIndex(m_shortcutComboBox->findData(Snippet::InvalidShortcut)); m_cursorMarkLineEdit->setText(Snippet::c_defaultCursorMark); diff --git a/src/widgets/dialogs/snippetinfowidget.h b/src/widgets/dialogs/snippetinfowidget.h index 03ac2fc2..9a6408ee 100644 --- a/src/widgets/dialogs/snippetinfowidget.h +++ b/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; diff --git a/src/widgets/dialogs/snippetpropertiesdialog.cpp b/src/widgets/dialogs/snippetpropertiesdialog.cpp index fc61040b..3fc37899 100644 --- a/src/widgets/dialogs/snippetpropertiesdialog.cpp +++ b/src/widgets/dialogs/snippetpropertiesdialog.cpp @@ -79,6 +79,7 @@ void SnippetPropertiesDialog::acceptedButtonClicked() bool SnippetPropertiesDialog::saveSnippetProperties() { auto snip = QSharedPointer::create(m_infoWidget->getName(), + m_infoWidget->getDescription(), m_infoWidget->getContent(), m_infoWidget->getShortcut(), m_infoWidget->shouldIndentAsFirstLine(), diff --git a/src/widgets/floatingwidget.cpp b/src/widgets/floatingwidget.cpp new file mode 100644 index 00000000..b7d0069b --- /dev/null +++ b/src/widgets/floatingwidget.cpp @@ -0,0 +1,32 @@ +#include "floatingwidget.h" + +#include + +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; +} diff --git a/src/widgets/floatingwidget.h b/src/widgets/floatingwidget.h new file mode 100644 index 00000000..db5c95ac --- /dev/null +++ b/src/widgets/floatingwidget.h @@ -0,0 +1,33 @@ +#ifndef FLOATINGWIDGET_H +#define FLOATINGWIDGET_H + +#include +#include + +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 diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 17088a6c..bc53a2b9 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -31,7 +31,6 @@ #include "editors/statuswidget.h" #include "editors/plantumlhelper.h" #include "editors/graphvizhelper.h" -#include 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); } diff --git a/src/widgets/markdownviewwindow.h b/src/widgets/markdownviewwindow.h index 4a584c91..a0148327 100644 --- a/src/widgets/markdownviewwindow.h +++ b/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(); diff --git a/src/widgets/quickselector.cpp b/src/widgets/quickselector.cpp new file mode 100644 index 00000000..5a4cbc5e --- /dev/null +++ b/src/widgets/quickselector.cpp @@ -0,0 +1,233 @@ +#include "quickselector.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 &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(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([®Exp](const QuickSelectorItem &p_item) { + if (p_item.m_name.indexOf(regExp) != -1) { + return true; + } + return false; + }); +} + +int QuickSelector::filterItems(const std::function &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; +} diff --git a/src/widgets/quickselector.h b/src/widgets/quickselector.h new file mode 100644 index 00000000..38b11f3f --- /dev/null +++ b/src/widgets/quickselector.h @@ -0,0 +1,74 @@ +#ifndef QUICKSELECTOR_H +#define QUICKSELECTOR_H + +#include "floatingwidget.h" + +#include +#include + +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 &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 &p_judge); + + QuickSelectorItem &getSelectorItem(const QListWidgetItem *p_item); + + QVector m_items; + + QLineEdit *m_searchLineEdit = nullptr; + + QListWidget *m_itemList = nullptr; + + QVariant m_selectedKey; + }; +} + +#endif // QUICKSELECTOR_H diff --git a/src/widgets/snippetpanel.cpp b/src/widgets/snippetpanel.cpp index 7244f83b..49e7a5bb 100644 --- a/src/widgets/snippetpanel.cpp +++ b/src/widgets/snippetpanel.cpp @@ -103,6 +103,7 @@ void SnippetPanel::updateSnippetList() } item->setData(Qt::UserRole, snippet->getName()); + item->setToolTip(snippet->getDescription()); } updateItemsCountLabel(); diff --git a/src/widgets/textviewwindow.cpp b/src/widgets/textviewwindow.cpp index a85fb8fb..76902e8b 100644 --- a/src/widgets/textviewwindow.cpp +++ b/src/widgets/textviewwindow.cpp @@ -15,7 +15,6 @@ #include #include "editors/statuswidget.h" #include -#include 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; - } - - m_editor->enterInsertModeIfApplicable(); - SnippetMgr::getInst().applySnippet(p_name, - m_editor->getTextEdit(), - SnippetMgr::generateOverrides(getBuffer())); + TextViewWindowHelper::applySnippet(this, p_name); +} + +void TextViewWindow::applySnippet() +{ + TextViewWindowHelper::applySnippet(this); +} + +QPoint TextViewWindow::getFloatingWidgetPosition() +{ + return TextViewWindowHelper::getFloatingWidgetPosition(this); } diff --git a/src/widgets/textviewwindow.h b/src/widgets/textviewwindow.h index 2f66fcfe..1af81465 100644 --- a/src/widgets/textviewwindow.h +++ b/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(); diff --git a/src/widgets/textviewwindowhelper.h b/src/widgets/textviewwindowhelper.h index 3d8b0e10..3b8c6933 100644 --- a/src/widgets/textviewwindowhelper.h +++ b/src/widgets/textviewwindowhelper.h @@ -2,11 +2,17 @@ #define TEXTVIEWWINDOWHELPER_H #include +#include +#include +#include #include #include #include #include +#include + +#include "quickselector.h" namespace vnotex { @@ -181,6 +187,104 @@ namespace vnotex p_win->m_editor->clearIncrementalSearchHighlight(); p_win->m_editor->clearSearchHighlight(); } + + template + 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 + 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 + 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 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 + 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); + } }; } diff --git a/src/widgets/viewwindow.cpp b/src/widgets/viewwindow.cpp index 15335d5c..9af9e94d 100644 --- a/src/widgets/viewwindow.cpp +++ b/src/widgets/viewwindow.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #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)); +} diff --git a/src/widgets/viewwindow.h b/src/widgets/viewwindow.h index 447963c6..717b6404 100644 --- a/src/widgets/viewwindow.h +++ b/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. diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index 4a8a93c0..3d1eb410 100644 --- a/src/widgets/widgets.pri +++ b/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 \