From 74cb54e02bdd851da827d6b39f1754308510771d Mon Sep 17 00:00:00 2001 From: Xianzhong Wang Date: Mon, 29 Jan 2018 06:03:44 +0800 Subject: [PATCH] [function] support advanced search in file list (#121) spec: when focus in file list, 1. type any character or digit will trigger the advanced search mode 2. type Esc to exit the search mode 3. type Enter or mouse select will also exit the search mode --- src/src.pro | 6 +- src/vfilelist.cpp | 4 +- src/vfilelist.h | 3 +- src/vlineedit.h | 1 - src/vlistwidget.cpp | 206 ++++++++++++++++++++++++++++++++++++++++++++ src/vlistwidget.h | 109 +++++++++++++++++++++++ 6 files changed, 324 insertions(+), 5 deletions(-) create mode 100644 src/vlistwidget.cpp create mode 100644 src/vlistwidget.h diff --git a/src/src.pro b/src/src.pro index 330b627b..cd88541f 100644 --- a/src/src.pro +++ b/src/src.pro @@ -108,7 +108,8 @@ SOURCES += main.cpp\ utils/vwebutils.cpp \ vlineedit.cpp \ vcart.cpp \ - vvimcmdlineedit.cpp + vvimcmdlineedit.cpp \ + vlistwidget.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -203,7 +204,8 @@ HEADERS += vmainwindow.h \ utils/vwebutils.h \ vlineedit.h \ vcart.h \ - vvimcmdlineedit.h + vvimcmdlineedit.h \ + vlistwidget.h RESOURCES += \ vnote.qrc \ diff --git a/src/vfilelist.cpp b/src/vfilelist.cpp index 5c17fe2f..08fe4013 100644 --- a/src/vfilelist.cpp +++ b/src/vfilelist.cpp @@ -61,7 +61,7 @@ VFileList::VFileList(QWidget *parent) void VFileList::setupUI() { - fileList = new QListWidget(this); + fileList = new VListWidget(this); fileList->setContextMenuPolicy(Qt::CustomContextMenu); fileList->setSelectionMode(QAbstractItemView::ExtendedSelection); fileList->setObjectName("FileList"); @@ -229,6 +229,7 @@ void VFileList::updateFileList() VNoteFile *file = files[i]; insertFileListItem(file); } + fileList->refresh(); } void VFileList::fileInfo() @@ -698,6 +699,7 @@ void VFileList::activateItem(QListWidgetItem *p_item, bool p_restoreFocus) // Qt seems not to update the QListWidget correctly. Manually force it to repaint. fileList->update(); + fileList->exitSearchMode(false); emit fileClicked(getVFile(p_item), g_config->getNoteOpenMode()); if (p_restoreFocus) { diff --git a/src/vfilelist.h b/src/vfilelist.h index 2238f75f..fcfdf83c 100644 --- a/src/vfilelist.h +++ b/src/vfilelist.h @@ -13,6 +13,7 @@ #include "vdirectory.h" #include "vnotefile.h" #include "vnavigationmode.h" +#include "vlistwidget.h" class QAction; class VNote; @@ -166,7 +167,7 @@ private: void activateItem(QListWidgetItem *p_item, bool p_restoreFocus = false); VEditArea *editArea; - QListWidget *fileList; + VListWidget *fileList; QPointer m_directory; // Magic number for clipboard operations. diff --git a/src/vlineedit.h b/src/vlineedit.h index 2cc1b367..39f0c4b8 100644 --- a/src/vlineedit.h +++ b/src/vlineedit.h @@ -12,7 +12,6 @@ public: VLineEdit(const QString &p_contents, QWidget *p_parent = nullptr); -protected: void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; }; diff --git a/src/vlistwidget.cpp b/src/vlistwidget.cpp new file mode 100644 index 00000000..c5d93a66 --- /dev/null +++ b/src/vlistwidget.cpp @@ -0,0 +1,206 @@ +#include "vlistwidget.h" +#include +#include +#include +#include +#include +#include "utils/vutils.h" + +const QString searchPrefix("Search for: "); +//TODO: make the style configuable +const QString c_searchKeyStyle("border:none; background:#eaeaea; color:%1;"); +const QString c_colorNotMatch("#fd676b"); +const QString c_colorMatch("grey"); + +VListWidget::VListWidget(QWidget *parent):QListWidget(parent), m_isInSearch(false), + m_curItemIdx(-1), m_curItem(nullptr) +{ + m_label = new QLabel(searchPrefix, this); + //TODO: make the style configuable + m_label->setStyleSheet(QString("color:gray;font-weight:bold;")); + m_searchKey = new VLineEdit(this); + m_searchKey->setStyleSheet(c_searchKeyStyle.arg(c_colorMatch)); + + QGridLayout *mainLayout = new QGridLayout; + QHBoxLayout *searchRowLayout = new QHBoxLayout; + searchRowLayout->addWidget(m_label); + searchRowLayout->addWidget(m_searchKey); + + mainLayout->addLayout(searchRowLayout, 0, 0, -1, 1, Qt::AlignBottom); + setLayout(mainLayout); + m_label->hide(); + m_searchKey->hide(); + + connect(m_searchKey, &VLineEdit::textChanged, + this, &VListWidget::handleSearchKeyChanged); + + m_delegateObj = new VItemDelegate(this); + setItemDelegate(m_delegateObj); +} + +void VListWidget::keyPressEvent(QKeyEvent *p_event) +{ + bool accept = false; + int modifiers = p_event->modifiers(); + + if (!m_isInSearch) { + bool isChar = (p_event->key() >= Qt::Key_A && p_event->key() <= Qt::Key_Z) + && (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier); + bool isDigit = (p_event->key() >= Qt::Key_0 && p_event->key() <= Qt::Key_9) + && (modifiers == Qt::NoModifier); + m_isInSearch = isChar || isDigit; + } + + bool moveUp = false; + switch (p_event->key()) { + case Qt::Key_J: + if (VUtils::isControlModifierForVim(modifiers)) { + // focus to next item/selection + QKeyEvent *targetEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier); + QCoreApplication::postEvent(this, targetEvent); + return; + } + break; + case Qt::Key_K: + if (VUtils::isControlModifierForVim(modifiers)) { + // focus to previous item/selection + QKeyEvent *targetEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier); + QCoreApplication::postEvent(this, targetEvent); + return; + } + break; + case Qt::Key_H: + if (VUtils::isControlModifierForVim(modifiers)) { + // Ctrl+H, delete one char + accept = false; + } + break; + case Qt::Key_F: + case Qt::Key_B: + // disable ctrl+f/b for the search key + accept = VUtils::isControlModifierForVim(modifiers); + break; + case Qt::Key_Escape: + m_isInSearch = false; + break; + case Qt::Key_Up: + moveUp = true; + // fall through + case Qt::Key_Down: + if (m_hitCount > 1) { + int newIdx = m_curItemIdx; + if (moveUp) { + newIdx = (newIdx - 1 + m_hitCount) % m_hitCount; + } else { + newIdx = (newIdx + 1) % m_hitCount; + } + if (newIdx != m_curItemIdx) { + if (m_curItemIdx != -1) { + m_hitItems[m_curItemIdx]->setSelected(false); + } + + m_curItemIdx = newIdx; + m_curItem = m_hitItems[m_curItemIdx]; + selectItem(m_curItem); + } + } + accept = true; + break; + } + if (m_isInSearch) { + enterSearchMode(); + } else { + exitSearchMode(); + } + + if (!accept) { + if (m_isInSearch) { + m_searchKey->keyPressEvent(p_event); + } else { + QListWidget::keyPressEvent(p_event); + } + } +} + +void VListWidget::enterSearchMode() { + m_label->show(); + m_searchKey->show(); + setSelectionMode(QAbstractItemView::SingleSelection); +} + +void VListWidget::exitSearchMode(bool restoreSelection) { + m_searchKey->clear(); + m_label->hide(); + m_searchKey->hide(); + setSelectionMode(QAbstractItemView::ExtendedSelection); + if (restoreSelection && m_curItem) { + selectItem(m_curItem); + } +} + + +void VListWidget::refresh() { + m_isInSearch = false; + m_hitItems = findItems("", Qt::MatchContains); + m_hitCount = m_hitItems.count(); + + for(const auto& it : selectedItems()) { + it->setSelected(false); + } + + if (m_hitCount > 0) { + if (selectedItems().isEmpty()) { + m_curItemIdx = 0; + m_curItem = m_hitItems.first(); + selectItem(m_curItem); + } + } else { + m_curItemIdx = -1; + m_curItem = nullptr; + } +} + +void VListWidget::clear() { + QListWidget::clear(); + m_hitCount = 0; + m_hitItems.clear(); + m_isInSearch = false; + m_curItem = nullptr; + m_curItemIdx = 0; + exitSearchMode(); +} + +void VListWidget::selectItem(QListWidgetItem *item) { + if (item) { + for(const auto& it : selectedItems()) { + it->setSelected(false); + } + setCurrentItem(item); + } +} + +void VListWidget::handleSearchKeyChanged(const QString& key) +{ + m_delegateObj->setSearchKey(key); + // trigger repaint & update + update(); + + m_hitItems = findItems(key, Qt::MatchContains); + if (key.isEmpty()) { + if (m_curItem) { + m_curItemIdx = m_hitItems.indexOf(m_curItem); + } + } else { + bool hasSearchResult = !m_hitItems.isEmpty(); + if (hasSearchResult) { + m_searchKey->setStyleSheet(c_searchKeyStyle.arg(c_colorMatch)); + + m_curItem = m_hitItems[0]; + setCurrentItem(m_curItem); + m_curItemIdx = 0; + } else { + m_searchKey->setStyleSheet(c_searchKeyStyle.arg(c_colorNotMatch)); + } + } + m_hitCount = m_hitItems.count(); +} diff --git a/src/vlistwidget.h b/src/vlistwidget.h new file mode 100644 index 00000000..f7c72afb --- /dev/null +++ b/src/vlistwidget.h @@ -0,0 +1,109 @@ +#ifndef VLISTWIDGET_H +#define VLISTWIDGET_H + +#include +#include +#include +#include +#include +#include +#include "vlineedit.h" +#include +#include + +class VItemDelegate : public QItemDelegate +{ +public: + explicit VItemDelegate(QObject *parent = Q_NULLPTR):QItemDelegate(parent), m_searchKey() { + } + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + painter->save(); + QPainter::CompositionMode oldCompMode = painter->compositionMode(); + // set background color + painter->setPen(QPen(Qt::NoPen)); + if (option.state & QStyle::State_Selected) { + // TODO: make it configuable + painter->setBrush(QBrush(QColor("#d3d3d3"))); + } else { + // use default brush + } + painter->drawRect(option.rect); + + Qt::GlobalColor hitPenColor = Qt::blue; + Qt::GlobalColor normalPenColor = Qt::black; + + // set text color + QVariant value = index.data(Qt::DisplayRole); + QRectF rect(option.rect), boundRect; + if (value.isValid()) { + QString text = value.toString(); + int idx; + bool isHit = !m_searchKey.isEmpty() && (idx=text.indexOf(m_searchKey, 0, Qt::CaseInsensitive)) != -1; + if (isHit) { + qDebug() << QString("highlight: %1 (with: %2)").arg(text).arg(m_searchKey); + // split the text by the search key + QString left = text.left(idx), right = text.mid(idx + m_searchKey.length()); + drawText(painter, normalPenColor, rect, Qt::AlignLeft, left, boundRect); + drawText(painter, hitPenColor, rect, Qt::AlignLeft, m_searchKey, boundRect); + + // highlight matched keyword + painter->setBrush(QBrush(QColor("#ffde7b"))); + painter->setCompositionMode(QPainter::CompositionMode_Multiply); + painter->setPen(Qt::NoPen); + painter->drawRect(boundRect); + painter->setCompositionMode(oldCompMode); + + drawText(painter, normalPenColor, rect, Qt::AlignLeft, right, boundRect); + } else { + drawText(painter, normalPenColor, rect, Qt::AlignLeft, text, boundRect); + } + } + + painter->restore(); + } + + void drawText(QPainter *painter, Qt::GlobalColor penColor, QRectF& rect, int flags, QString text, QRectF& boundRect) const { + if (!text.isEmpty()) { + painter->setPen(QPen(penColor)); + painter->drawText(rect, flags, text, &boundRect); + rect.adjust(boundRect.width(), 0, boundRect.width(), 0); + } + } + + void setSearchKey(const QString& key) { + m_searchKey = key; + } + +private: + QString m_searchKey; +}; + +class VListWidget : public QListWidget +{ +public: + explicit VListWidget(QWidget *parent = Q_NULLPTR); + void keyPressEvent(QKeyEvent *event); + void selectItem(QListWidgetItem *item); + void exitSearchMode(bool restoreSelection=true); + void enterSearchMode(); + void refresh(); + +public Q_SLOTS: + void handleSearchKeyChanged(const QString& updatedText); + void clear(); + + +private: + QLabel *m_label; + VLineEdit* m_searchKey; + bool m_isInSearch; + + VItemDelegate* m_delegateObj; + QList m_hitItems; // items that are matched by the search key + int m_hitCount; // how many items are matched, if no search key or key is empty string, all items are matched + int m_curItemIdx; // current selected item index + QListWidgetItem* m_curItem; // current selected item +}; + +#endif // VLISTWIDGET_H