Analyze and improve search in the file browser (again) (#6985)

Improves performance when searching in the file browser (confirmed with profiling using KCacheGrind), adds a search indicator at the bottom to let the user know a search is in progress, blacklists unnecessary system directories (speeding up both the search speed and potentially load times as well by reducing the number of filesystem entries to consider), and fixes an issue that causes not all of the search results to appear.
This commit is contained in:
saker
2023-11-18 20:47:15 -05:00
committed by GitHub
parent 17c919879f
commit fad0011508
5 changed files with 422 additions and 296 deletions

View File

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

View File

@@ -39,7 +39,10 @@
#include <QShortcut>
#include <QStringList>
#include <queue>
#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<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); }
}
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<QTreeWidgetItem*>(nullptr);
auto currentDir = baseDir;
for (const auto& pathPart : pathParts)
{
auto childCount = currentItem ? currentItem->childCount() : m_searchTreeWidget->topLevelItemCount();
auto childItem = static_cast<QTreeWidgetItem*>(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<Directory*>(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<FileItem*>(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<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;
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())
{

View File

@@ -0,0 +1,135 @@
/*
* 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