Bläddra i källkod

Add: Filter search function.

Satoshi Yoneda 3 veckor sedan
förälder
incheckning
6cbbb08742

+ 5 - 0
include/MainWindow.hpp

@@ -2,6 +2,8 @@
 #include "DatabaseManager.hpp"
 #include "ResultListModel.hpp"
 #include "ResultItemDelegate.hpp"
+#include "ResultFilterProxyModel.hpp"
+#include <QLineEdit>
 #include <unordered_set>
 #include <QCheckBox>
 #include <QCloseEvent>
@@ -42,6 +44,7 @@ private slots:
   void removeGroupFromView(int groupId); // 指定したグループをリストから除外
   void onContextMenuRequested(const std::string& path, int groupId, const QPoint& globalPos);
   void onFileDoubleClicked(const std::string& path);
+  void onSearchTextChanged(const QString &text);
 
 private:
   // 内部処理・初期化メソッド
@@ -81,6 +84,8 @@ private:
 
   ResultListModel *m_model;
   ResultItemDelegate *m_delegate;
+  ResultFilterProxyModel *m_proxyModel;
+  QLineEdit *m_searchBox;
 
   // 非同期(バックグラウンド)処理用オブジェクト
   QFutureWatcher<void> *m_scanWatcher; // 画像スキャンの進捗監視

+ 25 - 0
include/ResultFilterProxyModel.hpp

@@ -0,0 +1,25 @@
+#pragma once
+#include <QSortFilterProxyModel>
+#include <unordered_set>
+#include <QString>
+
+class ResultFilterProxyModel : public QSortFilterProxyModel {
+  Q_OBJECT
+
+public:
+  explicit ResultFilterProxyModel(QObject *parent = nullptr);
+
+  // Sets the search term and precalculates visible groups
+  void setSearchText(const QString &searchText);
+
+protected:
+  // Overrides default filtering to filter by our precalculated group visibility
+  bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
+
+private:
+  QString m_searchText;
+  std::unordered_set<int> m_visibleGroupIds;
+
+  // Helper method to scan the underlying model and populate m_visibleGroupIds
+  void updateVisibleGroups();
+};

+ 43 - 2
src/MainWindow.cpp

@@ -132,17 +132,30 @@ void MainWindow::setupUi() {
   }
   splitLayout->addWidget(m_dirList);
 
+  // Results section (Search box + List View)
+  auto *resultLayout = new QVBoxLayout();
+  m_searchBox = new QLineEdit();
+  m_searchBox->setPlaceholderText("Filter results by path or filename... (Press Esc to close)");
+  m_searchBox->setVisible(false);
+  resultLayout->addWidget(m_searchBox);
+
   // Results List View
   m_resultView = new QListView();
   m_model = new ResultListModel(this);
+  m_proxyModel = new ResultFilterProxyModel(this);
+  m_proxyModel->setSourceModel(m_model);
   m_delegate = new ResultItemDelegate(this);
   
-  m_resultView->setModel(m_model);
+  m_resultView->setModel(m_proxyModel);
   m_resultView->setItemDelegate(m_delegate);
   m_resultView->setSelectionMode(QAbstractItemView::NoSelection);
   m_resultView->setSpacing(5);
   
-  splitLayout->addWidget(m_resultView);
+  m_resultView->installEventFilter(this);
+  m_searchBox->installEventFilter(this);
+  
+  resultLayout->addWidget(m_resultView);
+  splitLayout->addLayout(resultLayout);
 
   mainLayout->addLayout(splitLayout);
 
@@ -160,6 +173,7 @@ void MainWindow::setupUi() {
           &MainWindow::onRemoveDirectory);
   connect(m_startScanBtn, &QPushButton::clicked, this,
           &MainWindow::onStartScan);
+  connect(m_searchBox, &QLineEdit::textChanged, this, &MainWindow::onSearchTextChanged);
   connect(m_clearBtn, &QPushButton::clicked, this, &MainWindow::onClearResults);
 
   connect(m_deselectBtn, &QPushButton::clicked, this, [this]() {
@@ -404,9 +418,36 @@ void MainWindow::onClearResults() {
 }
 
 bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
+  if (event->type() == QEvent::KeyPress) {
+    QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
+    if (obj == m_resultView) {
+      if ((keyEvent->modifiers() & Qt::ControlModifier) && keyEvent->key() == Qt::Key_F) {
+        m_searchBox->setVisible(true);
+        m_searchBox->setFocus();
+        return true;
+      } else if (keyEvent->key() == Qt::Key_F && keyEvent->modifiers() == Qt::NoModifier) {
+        m_searchBox->setVisible(true);
+        m_searchBox->setFocus();
+        return true;
+      }
+    } else if (obj == m_searchBox) {
+      if (keyEvent->key() == Qt::Key_Escape) {
+        m_searchBox->clear();
+        m_searchBox->setVisible(false);
+        m_resultView->setFocus();
+        return true;
+      }
+    }
+  }
   return QMainWindow::eventFilter(obj, event);
 }
 
+void MainWindow::onSearchTextChanged(const QString &text) {
+  if (m_proxyModel) {
+    m_proxyModel->setSearchText(text);
+  }
+}
+
 void MainWindow::onFileDoubleClicked(const std::string& path) {
   if (!path.empty()) {
     QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(path)));

+ 59 - 0
src/ResultFilterProxyModel.cpp

@@ -0,0 +1,59 @@
+#include "ResultFilterProxyModel.hpp"
+#include "ResultListModel.hpp"
+#include <QString>
+
+ResultFilterProxyModel::ResultFilterProxyModel(QObject *parent)
+    : QSortFilterProxyModel(parent) {}
+
+void ResultFilterProxyModel::setSearchText(const QString &searchText) {
+  if (m_searchText != searchText) {
+    m_searchText = searchText;
+    updateVisibleGroups();
+    invalidateFilter();
+  }
+}
+
+void ResultFilterProxyModel::updateVisibleGroups() {
+  m_visibleGroupIds.clear();
+
+  if (m_searchText.isEmpty()) {
+    return; // Fast path: all visible when no search text
+  }
+
+  // Determine which groups have at least one image matching the search text
+  ResultListModel *source = qobject_cast<ResultListModel *>(sourceModel());
+  if (!source)
+    return;
+
+  int rowCount = source->rowCount(QModelIndex());
+  for (int row = 0; row < rowCount; ++row) {
+    const auto &item = source->getItem(row);
+    if (item.type == ResultListItem::ImageRow) {
+      if (m_visibleGroupIds.find(item.groupId) != m_visibleGroupIds.end()) {
+        continue; // Group already marked as visible
+      }
+
+      for (const auto &img : item.images) {
+        QString pathStr = QString::fromStdString(img.path);
+        if (pathStr.contains(m_searchText, Qt::CaseInsensitive)) {
+          m_visibleGroupIds.insert(item.groupId);
+          break; // One match is enough for the entire group
+        }
+      }
+    }
+  }
+}
+
+bool ResultFilterProxyModel::filterAcceptsRow(
+    int source_row, const QModelIndex &source_parent) const {
+  if (m_searchText.isEmpty()) {
+    return true; // Show all if no filter
+  }
+
+  ResultListModel *source = qobject_cast<ResultListModel *>(sourceModel());
+  if (!source)
+    return true;
+
+  const auto &item = source->getItem(source_row);
+  return m_visibleGroupIds.find(item.groupId) != m_visibleGroupIds.end();
+}

+ 31 - 9
src/ResultItemDelegate.cpp

@@ -5,22 +5,42 @@
 #include <QFileInfo>
 #include <QMouseEvent>
 #include <QPainter>
+#include <QSortFilterProxyModel>
 #include <QToolTip>
 #include <cstddef>
 
+static const ResultListModel* getSourceModelAndIndex(const QModelIndex &index, QModelIndex &sourceIndex) {
+    if (auto proxy = qobject_cast<const QSortFilterProxyModel*>(index.model())) {
+        sourceIndex = proxy->mapToSource(index);
+        return qobject_cast<const ResultListModel*>(sourceIndex.model());
+    }
+    sourceIndex = index;
+    return qobject_cast<const ResultListModel*>(index.model());
+}
+
+static ResultListModel* getSourceModelAndIndex(QAbstractItemModel *model, const QModelIndex &index, QModelIndex &sourceIndex) {
+    if (auto proxy = qobject_cast<QSortFilterProxyModel*>(model)) {
+        sourceIndex = proxy->mapToSource(index);
+        return qobject_cast<ResultListModel*>(proxy->sourceModel());
+    }
+    sourceIndex = index;
+    return qobject_cast<ResultListModel*>(model);
+}
+
 ResultItemDelegate::ResultItemDelegate(QObject *parent)
     : QStyledItemDelegate(parent) {}
 
 void ResultItemDelegate::paint(QPainter *painter,
                                const QStyleOptionViewItem &option,
                                const QModelIndex &index) const {
-  const auto *model = qobject_cast<const ResultListModel *>(index.model());
+  QModelIndex sourceIndex;
+  const auto *model = getSourceModelAndIndex(index, sourceIndex);
   if (!model)
     return;
 
   painter->save();
 
-  const auto &item = model->getItem(index.row());
+  const auto &item = model->getItem(sourceIndex.row());
   if (item.type == ResultListItem::Header) {
     QRect r = option.rect;
     r.adjust(0, 5, 0, -5);
@@ -92,10 +112,11 @@ void ResultItemDelegate::paint(QPainter *painter,
 
 QSize ResultItemDelegate::sizeHint(const QStyleOptionViewItem &option,
                                    const QModelIndex &index) const {
-  const auto *model = qobject_cast<const ResultListModel *>(index.model());
+  QModelIndex sourceIndex;
+  const auto *model = getSourceModelAndIndex(index, sourceIndex);
   if (!model)
     return QSize(0, 0);
-  const auto &item = model->getItem(index.row());
+  const auto &item = model->getItem(sourceIndex.row());
   if (item.type == ResultListItem::Header) {
     return QSize(option.rect.width(), 40);
   } else {
@@ -109,11 +130,12 @@ bool ResultItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
   if (!event)
     return false;
 
-  auto *listModel = qobject_cast<ResultListModel *>(model);
+  QModelIndex sourceIndex;
+  auto *listModel = getSourceModelAndIndex(model, index, sourceIndex);
   if (!listModel)
     return false;
 
-  const auto &item = listModel->getItem(index.row());
+  const auto &item = listModel->getItem(sourceIndex.row());
   if (item.type != ResultListItem::ImageRow)
     return false;
 
@@ -167,12 +189,12 @@ bool ResultItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view,
     return false;
 
   if (event->type() == QEvent::ToolTip) {
-    const auto *listModel =
-        qobject_cast<const ResultListModel *>(index.model());
+    QModelIndex sourceIndex;
+    const auto *listModel = getSourceModelAndIndex(index, sourceIndex);
     if (!listModel)
       return false;
 
-    const auto &item = listModel->getItem(index.row());
+    const auto &item = listModel->getItem(sourceIndex.row());
     if (item.type != ResultListItem::ImageRow)
       return false;