浏览代码

[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
Xianzhong Wang 7 年之前
父节点
当前提交
74cb54e02b
共有 6 个文件被更改,包括 324 次插入5 次删除
  1. 4 2
      src/src.pro
  2. 3 1
      src/vfilelist.cpp
  3. 2 1
      src/vfilelist.h
  4. 0 1
      src/vlineedit.h
  5. 206 0
      src/vlistwidget.cpp
  6. 109 0
      src/vlistwidget.h

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

+ 3 - 1
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) {

+ 2 - 1
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<VDirectory> m_directory;
 
     // Magic number for clipboard operations.

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

+ 206 - 0
src/vlistwidget.cpp

@@ -0,0 +1,206 @@
+#include "vlistwidget.h"
+#include <QVBoxLayout>
+#include <QKeyEvent>
+#include <QCoreApplication>
+#include <QDebug>
+#include <QLabel>
+#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();
+}

+ 109 - 0
src/vlistwidget.h

@@ -0,0 +1,109 @@
+#ifndef VLISTWIDGET_H
+#define VLISTWIDGET_H
+
+#include <QListWidget>
+#include <QLineEdit>
+#include <QPainter>
+#include <QStyleOptionViewItem>
+#include <QModelIndex>
+#include <QItemDelegate>
+#include "vlineedit.h"
+#include <QDebug>
+#include <QLabel>
+
+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<QListWidgetItem*> 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