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

@@ -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);