[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
This commit is contained in:
Xianzhong Wang 2018-01-29 06:03:44 +08:00 committed by Le Tan
parent 77c664b773
commit 74cb54e02b
6 changed files with 324 additions and 5 deletions

View File

@ -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 \

View File

@ -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) {

View File

@ -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.

View File

@ -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
src/vlistwidget.cpp Normal file
View File

@ -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
src/vlistwidget.h Normal file
View File

@ -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