Analyze and improve search in the file browser (again) (#6985)

Improves performance when searching in the file browser (confirmed with profiling using KCacheGrind), adds a search indicator at the bottom to let the user know a search is in progress, blacklists unnecessary system directories (speeding up both the search speed and potentially load times as well by reducing the number of filesystem entries to consider), and fixes an issue that causes not all of the search results to appear.
This commit is contained in:
saker
2023-11-18 20:47:15 -05:00
committed by GitHub
parent 17c919879f
commit fad0011508
5 changed files with 422 additions and 296 deletions

View File

@@ -29,24 +29,16 @@
#include <QDir>
#include <QMutex>
#ifdef __MINGW32__
#include <mingw.condition_variable.h>
#include <mingw.mutex.h>
#include <mingw.thread.h>
#else
#include <condition_variable>
#include <mutex>
#include <thread>
#endif
#include "FileBrowserSearcher.h"
#include <QProgressBar>
#if (QT_VERSION >= QT_VERSION_CHECK(5,14,0))
#include <QRecursiveMutex>
#endif
#include <QTreeWidget>
#include "SideBarWidget.h"
#include "lmmsconfig.h"
class QLineEdit;
@@ -83,12 +75,25 @@ public:
~FileBrowser() override = default;
static QDir::Filters dirFilters();
static QStringList directoryBlacklist()
{
static auto s_blacklist = QStringList{
#ifdef LMMS_BUILD_LINUX
"/bin", "/boot", "/dev", "/etc", "/proc", "/run", "/sbin",
"/sys"
#endif
#ifdef LMMS_BUILD_WIN32
"C:\\Windows"
#endif
};
return s_blacklist;
}
static QDir::Filters dirFilters() { return QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot; }
static QDir::SortFlags sortFlags() { return QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase; }
private slots:
void reloadTree();
void expandItems( QTreeWidgetItem * item=nullptr, QList<QString> expandedDirs = QList<QString>() );
bool filterAndExpandItems(const QString & filter, QTreeWidgetItem * item = nullptr);
void giveFocusToFilter();
private:
@@ -99,7 +104,7 @@ private:
void saveDirectoriesStates();
void restoreDirectoriesStates();
void buildSearchTree(QStringList matches, QString id);
void buildSearchTree();
void onSearch(const QString& filter);
void toggleSearch(bool on);
@@ -108,6 +113,9 @@ private:
QLineEdit * m_filterEdit;
std::shared_ptr<FileBrowserSearcher::SearchFuture> m_currentSearch;
QProgressBar* m_searchIndicator = nullptr;
QString m_directories; //!< Directories to search, split with '*'
QString m_filter; //!< Filter as used in QDir::match()
@@ -183,54 +191,12 @@ private slots:
} ;
class FileBrowserSearcher : public QObject
{
Q_OBJECT
public:
struct SearchTask
{
QString directories;
QString userFilter;
QDir::Filters dirFilters;
QStringList nameFilters;
QString id;
};
FileBrowserSearcher();
~FileBrowserSearcher() noexcept override;
void search(SearchTask task);
void cancel();
bool inHiddenDirectory(const QString& path);
static FileBrowserSearcher* instance();
signals:
void searchComplete(QStringList matches, QString id);
private:
void run();
void filter();
SearchTask m_currentTask;
std::thread m_worker;
std::mutex m_runMutex;
std::mutex m_cancelMutex;
std::condition_variable m_runCond;
std::atomic<bool> m_cancel = false;
bool m_stopped = false;
bool m_run = false;
inline static std::unique_ptr<FileBrowserSearcher> s_instance = nullptr;
};
class Directory : public QTreeWidgetItem
{
public:
Directory( const QString & filename, const QString & path,
const QString & filter );
Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation = false);
void update();
@@ -275,7 +241,7 @@ private:
QString m_filter;
int m_dirCount;
bool m_disableEntryPopulation = false;
} ;

View File

@@ -0,0 +1,148 @@
/*
* FileBrowserSearcher.h - Batch processor for searching the filesystem
*
* Copyright (c) 2023 saker <sakertooth@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#ifndef LMMS_FILE_BROWSER_SEARCHER_H
#define LMMS_FILE_BROWSER_SEARCHER_H
#include <QHash>
#include <QString>
#include <QStringList>
#include <optional>
#include <queue>
#ifdef __MINGW32__
#include <mingw.condition_variable.h>
#include <mingw.mutex.h>
#include <mingw.thread.h>
#else
#include <condition_variable>
#include <mutex>
#include <thread>
#endif
namespace lmms::gui {
//! An active object that handles searching for files that match a certain filter across the file system.
class FileBrowserSearcher
{
public:
//! Number of milliseconds to wait for before a match should be processed by the user.
static constexpr int MillisecondsPerMatch = 1;
//! The future object for FileBrowserSearcher. It is used to track the current state of search operations, as
// well as retrieve matches.
class SearchFuture
{
public:
//! Possible state values of the future object.
enum class State
{
Idle,
Running,
Cancelled,
Completed
};
//! Constructs a future object using the specified filter, paths, and valid file extensions in the Idle state.
SearchFuture(const QString& filter, const QStringList& paths, const QStringList& extensions)
: m_filter(filter)
, m_paths(paths)
, m_extensions(extensions)
{
}
//! Retrieves a match from the match list.
auto match() -> QString
{
const auto lock = std::lock_guard{m_matchesMutex};
return m_matches.empty() ? QString{} : m_matches.takeFirst();
}
//! Returns the current state of this future object.
auto state() -> State { return m_state; }
//! Returns the filter used.
auto filter() -> const QString& { return m_filter; }
//! Returns the paths to filter.
auto paths() -> const QStringList& { return m_paths; }
//! Returns the valid file extensions.
auto extensions() -> const QStringList& { return m_extensions; }
private:
//! Adds a match to the match list.
auto addMatch(const QString& match) -> void
{
const auto lock = std::lock_guard{m_matchesMutex};
m_matches.append(match);
}
QString m_filter;
QStringList m_paths;
QStringList m_extensions;
QStringList m_matches;
std::mutex m_matchesMutex;
std::atomic<State> m_state = State::Idle;
friend FileBrowserSearcher;
};
~FileBrowserSearcher();
//! Enqueues a search to be ran by the worker thread.
//! Returns a future that the caller can use to track state and results of the operation.
auto search(const QString& filter, const QStringList& paths, const QStringList& extensions)
-> std::shared_ptr<SearchFuture>;
//! Sends a signal to cancel a running search.
auto cancel() -> void { m_cancelRunningSearch = true; }
//! Returns the global instance of the searcher object.
static auto instance() -> FileBrowserSearcher*
{
static auto s_instance = FileBrowserSearcher{};
return &s_instance;
}
private:
//! Event loop for the worker thread.
auto run() -> void;
//! Using Depth-first search (DFS), filters the specified path and adds any matches to the future list.
auto process(SearchFuture* searchFuture, const QString& path) -> bool;
std::queue<std::shared_ptr<SearchFuture>> m_searchQueue;
std::atomic<bool> m_cancelRunningSearch = false;
bool m_workerStopped = false;
std::mutex m_workerMutex;
std::condition_variable m_workerCond;
std::thread m_worker{[this] { run(); }};
};
} // namespace lmms::gui
#endif // LMMS_FILE_BROWSER_SEARCHER_H

View File

@@ -14,6 +14,7 @@ SET(LMMS_SRCS
gui/EffectView.cpp
gui/embed.cpp
gui/FileBrowser.cpp
gui/FileBrowserSearcher.cpp
gui/GuiApplication.cpp
gui/LadspaControlView.cpp
gui/LfoControllerDialog.cpp

View File

@@ -39,7 +39,10 @@
#include <QShortcut>
#include <QStringList>
#include <queue>
#include "FileBrowser.h"
#include "FileBrowserSearcher.h"
#include "AudioEngine.h"
#include "ConfigManager.h"
#include "DataFile.h"
@@ -130,7 +133,6 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter,
m_filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition);
connect(m_filterEdit, &QLineEdit::textEdited, this, &FileBrowser::onSearch);
connect(FileBrowserSearcher::instance(), &FileBrowserSearcher::searchComplete, this, &FileBrowser::buildSearchTree);
auto reload_btn = new QPushButton(embed::getIconPixmap("reload"), QString(), searchWidget);
reload_btn->setToolTip( tr( "Refresh list" ) );
@@ -149,6 +151,15 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter,
m_searchTreeWidget->hide();
addContentWidget(m_searchTreeWidget);
auto searchTimer = new QTimer(this);
connect(searchTimer, &QTimer::timeout, this, &FileBrowser::buildSearchTree);
searchTimer->start(FileBrowserSearcher::MillisecondsPerMatch);
m_searchIndicator = new QProgressBar(this);
m_searchIndicator->setMinimum(0);
m_searchIndicator->setMaximum(100);
addContentWidget(m_searchIndicator);
// Whenever the FileBrowser has focus, Ctrl+F should direct focus to its filter box.
auto filterFocusShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this, SLOT(giveFocusToFilter()));
filterFocusShortcut->setContext(Qt::WidgetWithChildrenShortcut);
@@ -159,11 +170,6 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter,
show();
}
QDir::Filters FileBrowser::dirFilters()
{
return QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot;
}
void FileBrowser::saveDirectoriesStates()
{
m_savedExpandedDirs = m_fileBrowserTreeWidget->expandedDirs();
@@ -174,72 +180,104 @@ void FileBrowser::restoreDirectoriesStates()
expandItems(nullptr, m_savedExpandedDirs);
}
void FileBrowser::buildSearchTree(QStringList matches, QString id)
void FileBrowser::buildSearchTree()
{
if (title() != id) { return; }
if (!m_currentSearch) { return; }
m_searchTreeWidget->clear();
const auto rootPaths = m_directories.split('*');
for (const auto& rootPath : rootPaths)
const auto match = m_currentSearch->match();
using State = FileBrowserSearcher::SearchFuture::State;
if ((m_currentSearch->state() == State::Completed && match.isEmpty())
|| m_currentSearch->state() == State::Cancelled)
{
const auto rootPathDir = QDir{rootPath};
const auto absoluteRootPath = rootPathDir.absolutePath();
m_currentSearch = nullptr;
m_searchIndicator->setMaximum(100);
return;
}
else if (match.isEmpty()) { return; }
for (const auto& match : matches)
{
if (!match.startsWith(absoluteRootPath)) { continue; }
const auto childInfo = QFileInfo{match};
const auto childName = childInfo.fileName();
const auto parentPath = childInfo.dir().path();
auto childWidget = static_cast<QTreeWidgetItem*>(nullptr);
if (childInfo.isDir())
{
auto dirChildWidget = new Directory(childName, parentPath, m_filter);
dirChildWidget->update();
childWidget = dirChildWidget;
}
else if (childInfo.isFile()) { childWidget = new FileItem(childName, parentPath); }
else { continue; }
const auto relativeParentPath = rootPathDir.relativeFilePath(parentPath);
if (relativeParentPath == ".")
{
m_searchTreeWidget->addTopLevelItem(childWidget);
if (childInfo.isDir()) { m_searchTreeWidget->expandItem(childWidget); }
continue;
}
const auto grandParentPath = QFileInfo{parentPath}.dir().path();
const auto parentItems = m_searchTreeWidget->findItems(relativeParentPath, Qt::MatchExactly);
if (parentItems.isEmpty())
{
auto parentItem = new Directory(relativeParentPath, grandParentPath, m_filter);
parentItem->addChild(childWidget);
m_searchTreeWidget->addTopLevelItem(parentItem);
m_searchTreeWidget->expandItem(parentItem);
}
else { parentItems[0]->addChild(childWidget); }
}
auto basePath = QString{};
for (const auto& path : m_directories.split('*'))
{
if (!match.startsWith(QDir{path}.absolutePath())) { continue; }
basePath = path;
break;
}
toggleSearch(true);
if (basePath.isEmpty()) { return; }
const auto baseDir = QDir{basePath};
const auto matchInfo = QFileInfo{match};
const auto matchRelativeToBasePath = baseDir.relativeFilePath(match);
auto pathParts = QDir::cleanPath(matchRelativeToBasePath).split("/");
auto currentItem = static_cast<QTreeWidgetItem*>(nullptr);
auto currentDir = baseDir;
for (const auto& pathPart : pathParts)
{
auto childCount = currentItem ? currentItem->childCount() : m_searchTreeWidget->topLevelItemCount();
auto childItem = static_cast<QTreeWidgetItem*>(nullptr);
for (int i = 0; i < childCount; ++i)
{
auto item = currentItem ? currentItem->child(i) : m_searchTreeWidget->topLevelItem(i);
if (item->text(0) == pathPart)
{
childItem = item;
break;
}
}
if (!childItem)
{
auto pathPartInfo = QFileInfo(currentDir, pathPart);
if (pathPartInfo.isDir())
{
// Only update directory (i.e., add entries) when it is the matched directory (so do not update
// parents since entries would be added to them that did not match the filter)
const auto disablePopulation = pathParts.indexOf(pathPart) < pathParts.size() - 1;
auto item = new Directory(pathPart, currentDir.path(), m_filter, disablePopulation);
currentItem ? currentItem->addChild(item) : m_searchTreeWidget->addTopLevelItem(item);
item->update();
if (disablePopulation) { m_searchTreeWidget->expandItem(item); }
childItem = item;
}
else
{
auto item = new FileItem(pathPart, currentDir.path());
currentItem ? currentItem->addChild(item) : m_searchTreeWidget->addTopLevelItem(item);
childItem = item;
}
}
currentItem = childItem;
if (!currentDir.cd(pathPart)) { break; }
}
}
void FileBrowser::onSearch(const QString& filter)
{
auto instance = FileBrowserSearcher::instance();
if (filter.isEmpty())
{
toggleSearch(false);
instance->cancel();
FileBrowserSearcher::instance()->cancel();
return;
}
instance->search({m_directories, filter, dirFilters(), m_filter.split(' '), title()});
auto directories = m_directories.split('*');
if (m_showUserContent && !m_showUserContent->isChecked()) { directories.removeAll(m_userDir); }
if (m_showFactoryContent && !m_showFactoryContent->isChecked()) { directories.removeAll(m_factoryDir); }
if (directories.isEmpty()) { return; }
m_searchTreeWidget->clear();
toggleSearch(true);
auto browserExtensions = m_filter;
const auto searchExtensions = browserExtensions.remove("*.").split(' ');
m_currentSearch = FileBrowserSearcher::instance()->search(filter, directories, searchExtensions);
}
void FileBrowser::toggleSearch(bool on)
@@ -248,90 +286,13 @@ void FileBrowser::toggleSearch(bool on)
{
m_searchTreeWidget->show();
m_fileBrowserTreeWidget->hide();
m_searchIndicator->setMaximum(0);
return;
}
m_searchTreeWidget->hide();
m_fileBrowserTreeWidget->show();
}
bool FileBrowser::filterAndExpandItems(const QString & filter, QTreeWidgetItem * item)
{
// Call with item = nullptr to filter the entire tree
if (item == nullptr)
{
// First search character so need to save current expanded directories
if (m_previousFilterValue.isEmpty())
{
saveDirectoriesStates();
}
m_previousFilterValue = filter;
}
if (filter.isEmpty())
{
// Restore previous expanded directories
if (item == nullptr)
{
restoreDirectoriesStates();
}
return false;
}
bool anyMatched = false;
int numChildren = item ? item->childCount() : m_fileBrowserTreeWidget->topLevelItemCount();
for (int i = 0; i < numChildren; ++i)
{
QTreeWidgetItem * it = item ? item->child( i ) : m_fileBrowserTreeWidget->topLevelItem(i);
auto d = dynamic_cast<Directory*>(it);
if (d)
{
if (it->text(0).contains(filter, Qt::CaseInsensitive))
{
it->setHidden(false);
it->setExpanded(true);
filterAndExpandItems(QString(), it);
anyMatched = true;
}
else
{
// Expanding is required when recursive to load in its contents, even if it's collapsed right afterward
it->setExpanded(true);
bool didMatch = filterAndExpandItems(filter, it);
it->setHidden(!didMatch);
it->setExpanded(didMatch);
anyMatched = anyMatched || didMatch;
}
}
else
{
auto f = dynamic_cast<FileItem*>(it);
if (f)
{
// File
bool didMatch = it->text(0).contains(filter, Qt::CaseInsensitive);
it->setHidden(!didMatch);
anyMatched = anyMatched || didMatch;
}
// A standard item (i.e. no file or directory item?)
else
{
// Hide if there's any filter
it->setHidden(!filter.isEmpty());
}
}
}
return anyMatched;
m_searchIndicator->setMaximum(100);
}
@@ -370,7 +331,7 @@ void FileBrowser::reloadTree()
}
else
{
filterAndExpandItems(m_filterEdit->text());
onSearch(m_filterEdit->text());
}
}
@@ -416,6 +377,8 @@ void FileBrowser::giveFocusToFilter()
void FileBrowser::addItems(const QString & path )
{
if (FileBrowser::directoryBlacklist().contains(path)) { return; }
if( m_dirsAsItems )
{
m_fileBrowserTreeWidget->addTopLevelItem( new Directory( path, QString(), m_filter ) );
@@ -429,6 +392,8 @@ void FileBrowser::addItems(const QString & path )
m_filter.split(' '), dirFilters(), QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase);
for (const auto& entry : entries)
{
if (FileBrowser::directoryBlacklist().contains(entry.absoluteFilePath())) { continue; }
QString fileName = entry.fileName();
if (entry.isDir())
{
@@ -1047,107 +1012,16 @@ void FileBrowserTreeWidget::updateDirectory(QTreeWidgetItem * item )
}
FileBrowserSearcher::FileBrowserSearcher()
: m_worker([this] { run(); })
{
}
FileBrowserSearcher::~FileBrowserSearcher() noexcept
{
m_cancel = true;
{
const auto runLock = std::lock_guard{m_runMutex};
m_stopped = true;
m_cancel = false;
}
m_runCond.notify_one();
m_worker.join();
}
void FileBrowserSearcher::search(SearchTask task)
{
m_cancel = true;
{
const auto runLock = std::lock_guard{m_runMutex};
m_currentTask = std::move(task);
m_run = true;
m_cancel = false;
}
m_runCond.notify_one();
}
void FileBrowserSearcher::cancel()
{
m_cancel = true;
}
void FileBrowserSearcher::run()
{
while (true)
{
auto lock = std::unique_lock{m_runMutex};
m_runCond.wait(lock, [this] { return m_run || m_stopped; });
if (m_stopped) { break; }
filter();
m_run = false;
}
}
void FileBrowserSearcher::filter()
{
const auto& [directories, userFilter, filters, nameFilters, id] = m_currentTask;
const auto paths = directories.split('*');
auto matches = QStringList{};
for (const auto& path : paths)
{
auto it = QDirIterator{path, nameFilters, filters, QDirIterator::Subdirectories};
while (it.hasNext())
{
it.next();
const auto name = it.fileName();
const auto path = it.filePath();
if (!inHiddenDirectory(path) && name.contains(userFilter, Qt::CaseInsensitive)) { matches.push_back(path); }
if (m_cancel) { return; }
}
}
emit searchComplete(matches, id);
}
FileBrowserSearcher* FileBrowserSearcher::instance()
{
if (!s_instance) { s_instance = std::make_unique<FileBrowserSearcher>(); }
return s_instance.get();
}
bool FileBrowserSearcher::inHiddenDirectory(const QString& path)
{
auto dir = QDir{path};
while (!dir.isRoot())
{
auto info = QFileInfo{dir.path()};
if (info.isHidden()) { return true; }
dir.cdUp();
}
return false;
}
QPixmap * Directory::s_folderPixmap = nullptr;
QPixmap * Directory::s_folderOpenedPixmap = nullptr;
QPixmap * Directory::s_folderLockedPixmap = nullptr;
Directory::Directory(const QString & filename, const QString & path,
const QString & filter ) :
QTreeWidgetItem( QStringList( filename ), TypeDirectoryItem ),
m_directories( path ),
m_filter( filter ),
m_dirCount( 0 )
Directory::Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation)
: QTreeWidgetItem(QStringList(filename), TypeDirectoryItem)
, m_directories(path)
, m_filter(filter)
, m_dirCount(0)
, m_disableEntryPopulation(disableEntryPopulation)
{
initPixmaps();
@@ -1199,7 +1073,7 @@ void Directory::update()
}
setIcon( 0, *s_folderOpenedPixmap );
if( !childCount() )
if (!m_disableEntryPopulation && !childCount())
{
m_dirCount = 0;
// for all paths leading here, add their items
@@ -1232,17 +1106,19 @@ void Directory::update()
bool Directory::addItems(const QString& path)
{
if (FileBrowser::directoryBlacklist().contains(path)) { return false; }
QDir thisDir(path);
if (!thisDir.isReadable()) { return false; }
treeWidget()->setUpdatesEnabled(false);
QFileInfoList entries = thisDir.entryInfoList(
m_filter.split(' '),
QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot,
QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase);
QFileInfoList entries
= thisDir.entryInfoList(m_filter.split(' '), FileBrowser::dirFilters(), FileBrowser::sortFlags());
for (const auto& entry : entries)
{
if (FileBrowser::directoryBlacklist().contains(entry.absoluteFilePath())) { continue; }
QString fileName = entry.fileName();
if (entry.isDir())
{

View File

@@ -0,0 +1,135 @@
/*
* FileBrowserSearcher.cpp - Batch processor for searching the filesystem
*
* Copyright (c) 2023 saker <sakertooth@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include "FileBrowserSearcher.h"
#include <QDir>
#include <stack>
#include "FileBrowser.h"
namespace lmms::gui {
FileBrowserSearcher::~FileBrowserSearcher()
{
m_cancelRunningSearch = true;
{
const auto lock = std::lock_guard{m_workerMutex};
m_workerStopped = true;
}
m_workerCond.notify_one();
m_worker.join();
}
auto FileBrowserSearcher::search(const QString& filter, const QStringList& paths, const QStringList& extensions)
-> std::shared_ptr<SearchFuture>
{
m_cancelRunningSearch = true;
auto future = std::make_shared<SearchFuture>(filter, paths, extensions);
{
const auto lock = std::lock_guard{m_workerMutex};
m_searchQueue.push(future);
m_cancelRunningSearch = false;
}
m_workerCond.notify_one();
return future;
}
auto FileBrowserSearcher::run() -> void
{
while (true)
{
auto lock = std::unique_lock{m_workerMutex};
m_workerCond.wait(lock, [this] { return m_workerStopped || !m_searchQueue.empty(); });
if (m_workerStopped) { return; }
const auto future = m_searchQueue.front();
future->m_state = SearchFuture::State::Running;
m_searchQueue.pop();
auto cancelled = false;
for (const auto& path : future->m_paths)
{
if (FileBrowser::directoryBlacklist().contains(path)) { continue; }
if (!process(future.get(), path))
{
future->m_state = SearchFuture::State::Cancelled;
cancelled = true;
break;
}
}
if (!cancelled) { future->m_state = SearchFuture::State::Completed; }
}
}
auto FileBrowserSearcher::process(SearchFuture* searchFuture, const QString& path) -> bool
{
auto stack = QFileInfoList{};
auto dir = QDir{path};
stack.append(dir.entryInfoList(FileBrowser::dirFilters(), FileBrowser::sortFlags()));
while (!stack.empty())
{
if (m_cancelRunningSearch)
{
m_cancelRunningSearch = false;
return false;
}
const auto info = stack.takeFirst();
const auto path = info.absoluteFilePath();
if (FileBrowser::directoryBlacklist().contains(path)) { continue; }
const auto name = info.fileName();
const auto validFile = info.isFile() && searchFuture->m_extensions.contains(info.suffix(), Qt::CaseInsensitive);
const auto passesFilter = name.contains(searchFuture->m_filter, Qt::CaseInsensitive);
// Only when a directory doesn't pass the filter should we search further
if (info.isDir() && !passesFilter)
{
dir.setPath(path);
auto entries = dir.entryInfoList(FileBrowser::dirFilters(), FileBrowser::sortFlags());
// Reverse to maintain the sorting within this directory when popped
std::reverse(entries.begin(), entries.end());
for (const auto& entry : entries)
{
stack.push_front(entry);
}
}
else if ((validFile || info.isDir()) && passesFilter) { searchFuture->addMatch(path); }
}
return true;
}
} // namespace lmms::gui