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