From bfeb781dc0c54bea79b7ff6cfcb17dedd353a275 Mon Sep 17 00:00:00 2001 From: saker Date: Wed, 29 May 2024 21:46:58 -0400 Subject: [PATCH] Redesign file browser searching (#7130) --- include/FileBrowser.h | 20 +++-- include/FileBrowserSearcher.h | 148 -------------------------------- include/FileSearch.h | 73 ++++++++++++++++ include/ThreadPool.h | 100 +++++++++++++++++++++ src/core/CMakeLists.txt | 2 + src/core/FileSearch.cpp | 96 +++++++++++++++++++++ src/core/ThreadPool.cpp | 85 ++++++++++++++++++ src/gui/CMakeLists.txt | 1 - src/gui/FileBrowser.cpp | 69 ++++++++------- src/gui/FileBrowserSearcher.cpp | 135 ----------------------------- 10 files changed, 403 insertions(+), 326 deletions(-) delete mode 100644 include/FileBrowserSearcher.h create mode 100644 include/FileSearch.h create mode 100644 include/ThreadPool.h create mode 100644 src/core/FileSearch.cpp create mode 100644 src/core/ThreadPool.cpp delete mode 100644 src/gui/FileBrowserSearcher.cpp diff --git a/include/FileBrowser.h b/include/FileBrowser.h index b0c8a51999..9185f5147d 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -28,10 +28,11 @@ #include #include #include -#include "embed.h" - -#include "FileBrowserSearcher.h" #include +#include + +#include "FileSearch.h" +#include "embed.h" #if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) #include @@ -76,9 +77,9 @@ public: ~FileBrowser() override = default; - static QStringList directoryBlacklist() + static QStringList excludedPaths() { - static auto s_blacklist = QStringList{ + static auto s_excludedPaths = QStringList{ #ifdef LMMS_BUILD_LINUX "/bin", "/boot", "/dev", "/etc", "/proc", "/run", "/sbin", "/sys" @@ -87,7 +88,7 @@ public: "C:\\Windows" #endif }; - return s_blacklist; + return s_excludedPaths; } static QDir::Filters dirFilters() { return QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot; } static QDir::SortFlags sortFlags() { return QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase; } @@ -105,16 +106,17 @@ private: void saveDirectoriesStates(); void restoreDirectoriesStates(); - void buildSearchTree(); + void foundSearchMatch(FileSearch* search, const QString& match); + void searchCompleted(FileSearch* search); void onSearch(const QString& filter); - void toggleSearch(bool on); + void displaySearch(bool on); FileBrowserTreeWidget * m_fileBrowserTreeWidget; FileBrowserTreeWidget * m_searchTreeWidget; QLineEdit * m_filterEdit; - std::shared_ptr m_currentSearch; + std::shared_ptr m_currentSearch; QProgressBar* m_searchIndicator = nullptr; QString m_directories; //!< Directories to search, split with '*' diff --git a/include/FileBrowserSearcher.h b/include/FileBrowserSearcher.h deleted file mode 100644 index 4f4d3ff1cb..0000000000 --- a/include/FileBrowserSearcher.h +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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/include/FileSearch.h b/include/FileSearch.h new file mode 100644 index 0000000000..fd311c9031 --- /dev/null +++ b/include/FileSearch.h @@ -0,0 +1,73 @@ +/* + * FileSearch.h - File system search task + * + * Copyright (c) 2024 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_SEARCH_H +#define LMMS_FILE_SEARCH_H + +#include +#include +#include + +namespace lmms { +//! A Qt object that encapsulates the operation of searching the file system. +class FileSearch : public QObject +{ + Q_OBJECT +public: + //! Number of milliseconds the search waits before signaling a matching result. + static constexpr int MillisecondsBetweenResults = 1; + + //! Create a `FileSearch` object that uses the specified string filter `filter` and extension filters in + //! `extensions` to search within the given `paths`. + //! `excludedPaths`, `dirFilters`, and `sortFlags` can optionally be specified to exclude certain directories, filter + //! out certain types of entries, and sort the matches. + FileSearch(const QString& filter, const QStringList& paths, const QStringList& extensions, + const QStringList& excludedPaths = {}, QDir::Filters dirFilters = QDir::Filters{}, + QDir::SortFlags sortFlags = QDir::SortFlags{}); + + //! Execute the search, emitting the `foundResult` signal when matches are found. + void operator()(); + + //! Cancel the search. + void cancel(); + +signals: + //! Emitted when a result is found when searching the file system. + void foundMatch(FileSearch* search, const QString& match); + + //! Emitted when the search completes. + void searchCompleted(FileSearch* search); + +private: + static auto isPathExcluded(const QString& path) -> bool; + QString m_filter; + QStringList m_paths; + QStringList m_extensions; + QStringList m_excludedPaths; + QDir::Filters m_dirFilters; + QDir::SortFlags m_sortFlags; + std::atomic m_cancel = false; +}; +} // namespace lmms +#endif // LMMS_FILE_SEARCH_H diff --git a/include/ThreadPool.h b/include/ThreadPool.h new file mode 100644 index 0000000000..b1d7900a68 --- /dev/null +++ b/include/ThreadPool.h @@ -0,0 +1,100 @@ +/* + * ThreadPool.h + * + * Copyright (c) 2024 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_THREAD_POOL_H +#define LMMS_THREAD_POOL_H + +#include +#include +#include +#include +#include + +#ifdef __MINGW32__ +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif + +namespace lmms { +//! A thread pool that can be used for asynchronous processing. +class ThreadPool +{ +public: + //! Destroys the `ThreadPool` object. + //! This blocks until all workers have finished executing. + ~ThreadPool(); + + //! Enqueue function `fn` with arguments `args` to be ran asynchronously. + template + auto enqueue(Fn&& fn, Args&&... args) -> std::future> + { + using ReturnType = std::invoke_result_t; + + auto promise = std::make_shared>(); + auto task = [promise, fn = std::forward(fn), args = std::make_tuple(std::forward(args)...)] + { + if constexpr (!std::is_same_v) + { + promise->set_value(std::apply(fn, args)); + return; + } + std::apply(fn, args); + promise->set_value(); + }; + + { + const auto lock = std::unique_lock{m_runMutex}; + m_queue.push(std::move(task)); + } + + m_runCond.notify_one(); + return promise->get_future(); + } + + //! Return the number of worker threads used. + auto numWorkers() const -> size_t; + + //! Return the global `ThreadPool` instance. + static auto instance() -> ThreadPool&; + +private: + ThreadPool(size_t numWorkers); + void run(); + std::vector m_workers; + std::queue> m_queue; + std::atomic m_done = false; + std::condition_variable m_runCond; + std::mutex m_runMutex; + inline static size_t s_numWorkers = std::thread::hardware_concurrency(); +}; +} // namespace lmms + +#endif // LMMS_THREAD_POOL_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1a4871fc72..9ebe2c3553 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -23,6 +23,7 @@ set(LMMS_SRCS core/Engine.cpp core/EnvelopeAndLfoParameters.cpp core/fft_helpers.cpp + core/FileSearch.cpp core/Mixer.cpp core/ImportFilter.cpp core/InlineAutomation.cpp @@ -76,6 +77,7 @@ set(LMMS_SRCS core/SerializingObject.cpp core/Song.cpp core/TempoSyncKnobModel.cpp + core/ThreadPool.cpp core/Timeline.cpp core/TimePos.cpp core/ToolPlugin.cpp diff --git a/src/core/FileSearch.cpp b/src/core/FileSearch.cpp new file mode 100644 index 0000000000..fe1efd97e9 --- /dev/null +++ b/src/core/FileSearch.cpp @@ -0,0 +1,96 @@ +/* + * FileSearch.cpp - File system search task + * + * Copyright (c) 2024 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 "FileSearch.h" + +#include +#include +#include + +#ifdef __MINGW32__ +#include +#else +#include +#endif + +namespace lmms { +FileSearch::FileSearch(const QString& filter, const QStringList& paths, const QStringList& extensions, + const QStringList& excludedPaths, QDir::Filters dirFilters, QDir::SortFlags sortFlags) + : m_filter(filter) + , m_paths(paths) + , m_extensions(extensions) + , m_excludedPaths(excludedPaths) + , m_dirFilters(dirFilters) + , m_sortFlags(sortFlags) +{ +} + +void FileSearch::operator()() +{ + auto stack = QFileInfoList{}; + for (const auto& path : m_paths) + { + if (m_excludedPaths.contains(path)) { continue; } + + auto dir = QDir{path}; + stack.append(dir.entryInfoList(m_dirFilters, m_sortFlags)); + + while (!stack.empty()) + { + if (m_cancel.load(std::memory_order_relaxed)) { return; } + + const auto info = stack.takeFirst(); + const auto entryPath = info.absoluteFilePath(); + if (m_excludedPaths.contains(entryPath)) { continue; } + + const auto name = info.fileName(); + const auto validFile = info.isFile() && m_extensions.contains(info.suffix(), Qt::CaseInsensitive); + const auto passesFilter = name.contains(m_filter, Qt::CaseInsensitive); + + if ((validFile || info.isDir()) && passesFilter) + { + std::this_thread::sleep_for(std::chrono::milliseconds{MillisecondsBetweenResults}); + emit foundMatch(this, entryPath); + } + + if (info.isDir()) + { + dir.setPath(entryPath); + const auto entries = dir.entryInfoList(m_dirFilters, m_sortFlags); + + // Reverse to maintain the sorting within this directory when popped + std::for_each(entries.rbegin(), entries.rend(), [&stack](const auto& entry) { stack.push_front(entry); }); + } + } + } + + emit searchCompleted(this); +} + +void FileSearch::cancel() +{ + m_cancel.store(true, std::memory_order_relaxed); +} + +} // namespace lmms diff --git a/src/core/ThreadPool.cpp b/src/core/ThreadPool.cpp new file mode 100644 index 0000000000..2e5f00df00 --- /dev/null +++ b/src/core/ThreadPool.cpp @@ -0,0 +1,85 @@ +/* + * ThreadPool.cpp + * + * Copyright (c) 2024 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 "ThreadPool.h" + +#include +#include +#include + +namespace lmms { +ThreadPool::ThreadPool(size_t numWorkers) +{ + assert(numWorkers > 0); + + m_workers.reserve(numWorkers); + for (size_t i = 0; i < numWorkers; ++i) + { + m_workers.emplace_back([this] { run(); }); + } +} + +ThreadPool::~ThreadPool() +{ + { + const auto lock = std::unique_lock{m_runMutex}; + m_done = true; + } + + m_runCond.notify_all(); + + for (auto& worker : m_workers) + { + if (worker.joinable()) { worker.join(); } + } +} + +auto ThreadPool::numWorkers() const -> size_t +{ + return m_workers.size(); +} + +void ThreadPool::run() +{ + while (!m_done) + { + std::function task; + { + auto lock = std::unique_lock{m_runMutex}; + m_runCond.wait(lock, [this] { return !m_queue.empty() || m_done; }); + + if (m_done) { break; } + task = m_queue.front(); + m_queue.pop(); + } + task(); + } +} + +auto ThreadPool::instance() -> ThreadPool& +{ + static auto s_pool = ThreadPool{s_numWorkers}; + return s_pool; +} + +} // namespace lmms diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index f6115dbf66..2485b92d2a 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -14,7 +14,6 @@ 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 89201cb044..04f7ff46f1 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -31,23 +31,22 @@ #include #include #include -#include -#include #include #include +#include #include +#include #include #include - +#include #include -#include "FileBrowser.h" -#include "FileBrowserSearcher.h" #include "AudioEngine.h" #include "ConfigManager.h" #include "DataFile.h" -#include "embed.h" #include "Engine.h" +#include "FileBrowser.h" +#include "FileSearch.h" #include "GuiApplication.h" #include "ImportFilter.h" #include "Instrument.h" @@ -65,6 +64,8 @@ #include "Song.h" #include "StringPairDrag.h" #include "TextFloat.h" +#include "ThreadPool.h" +#include "embed.h" namespace lmms::gui { @@ -152,10 +153,6 @@ 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); @@ -181,20 +178,10 @@ void FileBrowser::restoreDirectoriesStates() expandItems(m_savedExpandedDirs); } -void FileBrowser::buildSearchTree() +void FileBrowser::foundSearchMatch(FileSearch* search, const QString& match) { - if (!m_currentSearch) { return; } - - const auto match = m_currentSearch->match(); - using State = FileBrowserSearcher::SearchFuture::State; - if ((m_currentSearch->state() == State::Completed && match.isEmpty()) - || m_currentSearch->state() == State::Cancelled) - { - m_currentSearch = nullptr; - m_searchIndicator->setMaximum(100); - return; - } - else if (match.isEmpty()) { return; } + assert(search != nullptr); + if (m_currentSearch.get() != search) { return; } auto basePath = QString{}; for (const auto& path : m_directories.split('*')) @@ -258,13 +245,22 @@ void FileBrowser::buildSearchTree() } } - +void FileBrowser::searchCompleted(FileSearch* search) +{ + assert(search != nullptr); + if (m_currentSearch.get() != search) { return; } + + m_currentSearch.reset(); + m_searchIndicator->setMaximum(100); +} + void FileBrowser::onSearch(const QString& filter) { + if (m_currentSearch) { m_currentSearch->cancel(); } + if (filter.isEmpty()) { - toggleSearch(false); - FileBrowserSearcher::instance()->cancel(); + displaySearch(false); return; } @@ -274,14 +270,21 @@ void FileBrowser::onSearch(const QString& filter) if (directories.isEmpty()) { return; } m_searchTreeWidget->clear(); - toggleSearch(true); + displaySearch(true); auto browserExtensions = m_filter; const auto searchExtensions = browserExtensions.remove("*.").split(' '); - m_currentSearch = FileBrowserSearcher::instance()->search(filter, directories, searchExtensions); + + auto search = std::make_shared( + filter, directories, searchExtensions, excludedPaths(), dirFilters(), sortFlags()); + connect(search.get(), &FileSearch::foundMatch, this, &FileBrowser::foundSearchMatch, Qt::QueuedConnection); + connect(search.get(), &FileSearch::searchCompleted, this, &FileBrowser::searchCompleted, Qt::QueuedConnection); + + m_currentSearch = search; + ThreadPool::instance().enqueue([search] { (*search)(); }); } -void FileBrowser::toggleSearch(bool on) +void FileBrowser::displaySearch(bool on) { if (on) { @@ -376,7 +379,7 @@ void FileBrowser::giveFocusToFilter() void FileBrowser::addItems(const QString & path ) { - if (FileBrowser::directoryBlacklist().contains(path)) { return; } + if (FileBrowser::excludedPaths().contains(path)) { return; } if( m_dirsAsItems ) { @@ -391,7 +394,7 @@ 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; } + if (FileBrowser::excludedPaths().contains(entry.absoluteFilePath())) { continue; } QString fileName = entry.fileName(); if (entry.isDir()) @@ -1066,7 +1069,7 @@ void Directory::update() bool Directory::addItems(const QString& path) { - if (FileBrowser::directoryBlacklist().contains(path)) { return false; } + if (FileBrowser::excludedPaths().contains(path)) { return false; } QDir thisDir(path); if (!thisDir.isReadable()) { return false; } @@ -1077,7 +1080,7 @@ bool Directory::addItems(const QString& path) = thisDir.entryInfoList(m_filter.split(' '), FileBrowser::dirFilters(), FileBrowser::sortFlags()); for (const auto& entry : entries) { - if (FileBrowser::directoryBlacklist().contains(entry.absoluteFilePath())) { continue; } + if (FileBrowser::excludedPaths().contains(entry.absoluteFilePath())) { continue; } QString fileName = entry.fileName(); if (entry.isDir()) diff --git a/src/gui/FileBrowserSearcher.cpp b/src/gui/FileBrowserSearcher.cpp deleted file mode 100644 index 80c2380580..0000000000 --- a/src/gui/FileBrowserSearcher.cpp +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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