Browse Source

[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 years ago
parent
commit
74cb54e02b
6 changed files with 324 additions and 5 deletions
  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 \
     utils/vwebutils.cpp \
     vlineedit.cpp \
     vlineedit.cpp \
     vcart.cpp \
     vcart.cpp \
-    vvimcmdlineedit.cpp
+    vvimcmdlineedit.cpp \
+    vlistwidget.cpp
 
 
 HEADERS  += vmainwindow.h \
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
     vdirectorytree.h \
@@ -203,7 +204,8 @@ HEADERS  += vmainwindow.h \
     utils/vwebutils.h \
     utils/vwebutils.h \
     vlineedit.h \
     vlineedit.h \
     vcart.h \
     vcart.h \
-    vvimcmdlineedit.h
+    vvimcmdlineedit.h \
+    vlistwidget.h
 
 
 RESOURCES += \
 RESOURCES += \
     vnote.qrc \
     vnote.qrc \

+ 3 - 1
src/vfilelist.cpp

@@ -61,7 +61,7 @@ VFileList::VFileList(QWidget *parent)
 
 
 void VFileList::setupUI()
 void VFileList::setupUI()
 {
 {
-    fileList = new QListWidget(this);
+    fileList = new VListWidget(this);
     fileList->setContextMenuPolicy(Qt::CustomContextMenu);
     fileList->setContextMenuPolicy(Qt::CustomContextMenu);
     fileList->setSelectionMode(QAbstractItemView::ExtendedSelection);
     fileList->setSelectionMode(QAbstractItemView::ExtendedSelection);
     fileList->setObjectName("FileList");
     fileList->setObjectName("FileList");
@@ -229,6 +229,7 @@ void VFileList::updateFileList()
         VNoteFile *file = files[i];
         VNoteFile *file = files[i];
         insertFileListItem(file);
         insertFileListItem(file);
     }
     }
+    fileList->refresh();
 }
 }
 
 
 void VFileList::fileInfo()
 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.
     // Qt seems not to update the QListWidget correctly. Manually force it to repaint.
     fileList->update();
     fileList->update();
+    fileList->exitSearchMode(false);
     emit fileClicked(getVFile(p_item), g_config->getNoteOpenMode());
     emit fileClicked(getVFile(p_item), g_config->getNoteOpenMode());
 
 
     if (p_restoreFocus) {
     if (p_restoreFocus) {

+ 2 - 1
src/vfilelist.h

@@ -13,6 +13,7 @@
 #include "vdirectory.h"
 #include "vdirectory.h"
 #include "vnotefile.h"
 #include "vnotefile.h"
 #include "vnavigationmode.h"
 #include "vnavigationmode.h"
+#include "vlistwidget.h"
 
 
 class QAction;
 class QAction;
 class VNote;
 class VNote;
@@ -166,7 +167,7 @@ private:
     void activateItem(QListWidgetItem *p_item, bool p_restoreFocus = false);
     void activateItem(QListWidgetItem *p_item, bool p_restoreFocus = false);
 
 
     VEditArea *editArea;
     VEditArea *editArea;
-    QListWidget *fileList;
+    VListWidget *fileList;
     QPointer<VDirectory> m_directory;
     QPointer<VDirectory> m_directory;
 
 
     // Magic number for clipboard operations.
     // Magic number for clipboard operations.

+ 0 - 1
src/vlineedit.h

@@ -12,7 +12,6 @@ public:
 
 
     VLineEdit(const QString &p_contents, QWidget *p_parent = nullptr);
     VLineEdit(const QString &p_contents, QWidget *p_parent = nullptr);
 
 
-protected:
     void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
     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