Redesign file browser searching (#7130)
This commit is contained in:
@@ -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
|
||||
|
||||
96
src/core/FileSearch.cpp
Normal file
96
src/core/FileSearch.cpp
Normal file
@@ -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 <atomic>
|
||||
#include <chrono>
|
||||
#include <lmmsconfig.h>
|
||||
|
||||
#ifdef __MINGW32__
|
||||
#include <mingw.thread.h>
|
||||
#else
|
||||
#include <thread>
|
||||
#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
|
||||
85
src/core/ThreadPool.cpp
Normal file
85
src/core/ThreadPool.cpp
Normal file
@@ -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 <cassert>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
|
||||
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<void()> 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
|
||||
@@ -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
|
||||
|
||||
@@ -31,23 +31,22 @@
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QMdiArea>
|
||||
#include <QMdiSubWindow>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QShortcut>
|
||||
#include <QStringList>
|
||||
|
||||
#include <cassert>
|
||||
#include <queue>
|
||||
|
||||
#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<FileSearch>(
|
||||
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())
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
Reference in New Issue
Block a user