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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user