Improve search performance in FileBrowser (#6962)

Improves the search performance of the file browser by delegating the search to a worker thread. The main thread then builds the tree and displays it to the user.
This commit is contained in:
saker
2023-11-10 14:26:31 -05:00
committed by GitHub
parent 89c98a77a5
commit 5596abb66a
3 changed files with 262 additions and 20 deletions

View File

@@ -28,6 +28,17 @@
#include <QCheckBox>
#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
#if (QT_VERSION >= QT_VERSION_CHECK(5,14,0))
#include <QRecursiveMutex>
#endif
@@ -72,6 +83,8 @@ public:
~FileBrowser() override = default;
static QDir::Filters dirFilters();
private slots:
void reloadTree();
void expandItems( QTreeWidgetItem * item=nullptr, QList<QString> expandedDirs = QList<QString>() );
@@ -86,7 +99,12 @@ private:
void saveDirectoriesStates();
void restoreDirectoriesStates();
void buildSearchTree(QStringList matches, QString id);
void onSearch(const QString& filter);
void toggleSearch(bool on);
FileBrowserTreeWidget * m_fileBrowserTreeWidget;
FileBrowserTreeWidget * m_searchTreeWidget;
QLineEdit * m_filterEdit;
@@ -165,6 +183,46 @@ 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;
};
@@ -274,6 +332,7 @@ public:
QString extension();
static QString extension( const QString & file );
static QString defaultFilters();
private:

View File

@@ -23,8 +23,11 @@
*
*/
#include "FileBrowser.h"
#include <QApplication>
#include <QDesktopServices>
#include <QDirIterator>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLineEdit>
@@ -126,7 +129,8 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter,
m_filterEdit->setClearButtonEnabled(true);
m_filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition);
connect(m_filterEdit, &QLineEdit::textEdited, [this](const QString & filter) { filterAndExpandItems(filter); });
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" ) );
@@ -141,6 +145,10 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter,
m_fileBrowserTreeWidget = new FileBrowserTreeWidget( contentParent() );
addContentWidget( m_fileBrowserTreeWidget );
m_searchTreeWidget = new FileBrowserTreeWidget(contentParent());
m_searchTreeWidget->hide();
addContentWidget(m_searchTreeWidget);
// 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);
@@ -151,6 +159,11 @@ 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();
@@ -161,6 +174,87 @@ void FileBrowser::restoreDirectoriesStates()
expandItems(nullptr, m_savedExpandedDirs);
}
void FileBrowser::buildSearchTree(QStringList matches, QString id)
{
if (title() != id) { return; }
m_searchTreeWidget->clear();
const auto rootPaths = m_directories.split('*');
for (const auto& rootPath : rootPaths)
{
const auto rootPathDir = QDir{rootPath};
const auto absoluteRootPath = rootPathDir.absolutePath();
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); }
}
}
toggleSearch(true);
}
void FileBrowser::onSearch(const QString& filter)
{
auto instance = FileBrowserSearcher::instance();
if (filter.isEmpty())
{
toggleSearch(false);
instance->cancel();
return;
}
instance->search({m_directories, filter, dirFilters(), m_filter.split(' '), title()});
}
void FileBrowser::toggleSearch(bool on)
{
if (on)
{
m_searchTreeWidget->show();
m_fileBrowserTreeWidget->hide();
return;
}
m_searchTreeWidget->hide();
m_fileBrowserTreeWidget->show();
}
bool FileBrowser::filterAndExpandItems(const QString & filter, QTreeWidgetItem * item)
{
// Call with item = nullptr to filter the entire tree
@@ -332,9 +426,7 @@ void FileBrowser::addItems(const QString & path )
QDir cdir(path);
if (!cdir.isReadable()) { return; }
QFileInfoList entries = cdir.entryInfoList(
m_filter.split(' '),
QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot,
QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase);
m_filter.split(' '), dirFilters(), QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase);
for (const auto& entry : entries)
{
QString fileName = entry.fileName();
@@ -956,7 +1048,93 @@ 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;
@@ -1276,5 +1454,18 @@ QString FileItem::extension(const QString & file )
return QFileInfo( file ).suffix().toLower();
}
QString FileItem::defaultFilters()
{
// TODO: Supported extensions should be in a centralized location
auto simpleExtensions
= QString{"*.mmp *.mpt *.mmpz *.xpf *.xml *.xiz *.sf2 *.sf3 *.pat *.mid *.midi *.rmi *.dll *.lv2"};
#ifdef LMMS_BUILD_LINUX
simpleExtensions += " *.so";
#endif
auto audioExtensions = QString{"*.wav *.ogg *.ds *.flac *.spx *.voc *.aif *.aiff *.au *.raw *.wav *.ogg *.ds "
"*.flac *.spx *.voc *.aif *.aiff *.au *.raw"};
return simpleExtensions + " " + audioExtensions;
}
} // namespace lmms::gui

View File

@@ -118,14 +118,10 @@ MainWindow::MainWindow() :
splitter, false, true,
confMgr->userProjectsDir(),
confMgr->factoryProjectsDir()));
sideBar->appendTab( new FileBrowser(
confMgr->userSamplesDir() + "*" +
confMgr->factorySamplesDir(),
"*", tr( "My Samples" ),
embed::getIconPixmap( "sample_file" ).transformed( QTransform().rotate( 90 ) ),
splitter, false, true,
confMgr->userSamplesDir(),
confMgr->factorySamplesDir()));
sideBar->appendTab(
new FileBrowser(confMgr->userSamplesDir() + "*" + confMgr->factorySamplesDir(), FileItem::defaultFilters(),
tr("My Samples"), embed::getIconPixmap("sample_file").transformed(QTransform().rotate(90)), splitter, false,
true, confMgr->userSamplesDir(), confMgr->factorySamplesDir()));
sideBar->appendTab( new FileBrowser(
confMgr->userPresetsDir() + "*" +
confMgr->factoryPresetsDir(),
@@ -135,11 +131,8 @@ MainWindow::MainWindow() :
splitter , false, true,
confMgr->userPresetsDir(),
confMgr->factoryPresetsDir()));
sideBar->appendTab( new FileBrowser( QDir::homePath(), "*",
tr( "My Home" ),
embed::getIconPixmap( "home" ).transformed( QTransform().rotate( 90 ) ),
splitter, false, false ) );
sideBar->appendTab(new FileBrowser(QDir::homePath(), FileItem::defaultFilters(), tr("My Home"),
embed::getIconPixmap("home").transformed(QTransform().rotate(90)), splitter, false, false));
QStringList root_paths;
QString title = tr( "Root directory" );
@@ -161,9 +154,8 @@ MainWindow::MainWindow() :
}
#endif
sideBar->appendTab( new FileBrowser( root_paths.join( "*" ), "*", title,
embed::getIconPixmap( "computer" ).transformed( QTransform().rotate( 90 ) ),
splitter, dirs_as_items) );
sideBar->appendTab(new FileBrowser(root_paths.join("*"), FileItem::defaultFilters(), title,
embed::getIconPixmap("computer").transformed(QTransform().rotate(90)), splitter, dirs_as_items));
m_workspace = new QMdiArea(splitter);