diff --git a/include/FileBrowser.h b/include/FileBrowser.h index 02fec2719..4d6fa745e 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -29,24 +29,16 @@ #include #include -#ifdef __MINGW32__ -#include -#include -#include -#else -#include -#include -#include -#endif +#include "FileBrowserSearcher.h" +#include #if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) #include #endif #include - #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 expandedDirs = QList() ); - 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 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 m_cancel = false; - bool m_stopped = false; - bool m_run = false; - inline static std::unique_ptr 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; } ; diff --git a/include/FileBrowserSearcher.h b/include/FileBrowserSearcher.h new file mode 100644 index 000000000..4f4d3ff1c --- /dev/null +++ b/include/FileBrowserSearcher.h @@ -0,0 +1,148 @@ +/* + * FileBrowserSearcher.h - Batch processor for searching the filesystem + * + * Copyright (c) 2023 saker + * + * 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 +#include +#include +#include +#include + +#ifdef __MINGW32__ +#include +#include +#include +#else +#include +#include +#include +#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 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; + + //! 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> m_searchQueue; + std::atomic 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 diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 1e809e9d7..e050d14bd 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -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 diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index dc16a3bac..f0b7e83ec 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -39,7 +39,10 @@ #include #include +#include + #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(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(nullptr); + auto currentDir = baseDir; + + for (const auto& pathPart : pathParts) + { + auto childCount = currentItem ? currentItem->childCount() : m_searchTreeWidget->topLevelItemCount(); + auto childItem = static_cast(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(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(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(); } - 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()) { diff --git a/src/gui/FileBrowserSearcher.cpp b/src/gui/FileBrowserSearcher.cpp new file mode 100644 index 000000000..80c238058 --- /dev/null +++ b/src/gui/FileBrowserSearcher.cpp @@ -0,0 +1,135 @@ +/* + * FileBrowserSearcher.cpp - Batch processor for searching the filesystem + * + * Copyright (c) 2023 saker + * + * 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 +#include + +#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 +{ + m_cancelRunningSearch = true; + auto future = std::make_shared(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