From c51b81f764f3cf908b921e4ef0f89d302dbba1b8 Mon Sep 17 00:00:00 2001 From: saker Date: Sun, 2 Feb 2025 02:00:07 -0500 Subject: [PATCH] Run searches on UI thread, remove hierarchy search results, simplify search logic --- include/FileBrowser.h | 9 +-- include/FileSearch.h | 73 -------------------- src/core/CMakeLists.txt | 1 - src/core/FileSearch.cpp | 92 ------------------------- src/gui/FileBrowser.cpp | 149 +++++++++++----------------------------- 5 files changed, 42 insertions(+), 282 deletions(-) delete mode 100644 include/FileSearch.h delete mode 100644 src/core/FileSearch.cpp diff --git a/include/FileBrowser.h b/include/FileBrowser.h index 9193da5e405..5d130e732b8 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -29,9 +29,7 @@ #include #include #include -#include -#include "FileSearch.h" #include "embed.h" #if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) @@ -106,10 +104,7 @@ private slots: void saveDirectoriesStates(); void restoreDirectoriesStates(); - void foundSearchMatch(FileSearch* search, const QString& match); - void searchCompleted(FileSearch* search); void onSearch(const QString& filter); - void displaySearch(bool on); void addContentCheckBox(); @@ -118,7 +113,6 @@ private slots: QLineEdit * m_filterEdit; - std::shared_ptr m_currentSearch; QProgressBar* m_searchIndicator = nullptr; QString m_directories; //!< Directories to search, split with '*' @@ -204,7 +198,7 @@ private slots: class Directory : public QTreeWidgetItem { public: - Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation = false); + Directory(const QString& filename, const QString& path, const QString& filter); void update(); @@ -247,7 +241,6 @@ class Directory : public QTreeWidgetItem QString m_filter; int m_dirCount; - bool m_disableEntryPopulation = false; } ; diff --git a/include/FileSearch.h b/include/FileSearch.h deleted file mode 100644 index fd311c9031e..00000000000 --- a/include/FileSearch.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - * FileSearch.h - 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. - * - */ - -#ifndef LMMS_FILE_SEARCH_H -#define LMMS_FILE_SEARCH_H - -#include -#include -#include - -namespace lmms { -//! A Qt object that encapsulates the operation of searching the file system. -class FileSearch : public QObject -{ - Q_OBJECT -public: - //! Number of milliseconds the search waits before signaling a matching result. - static constexpr int MillisecondsBetweenResults = 1; - - //! Create a `FileSearch` object that uses the specified string filter `filter` and extension filters in - //! `extensions` to search within the given `paths`. - //! `excludedPaths`, `dirFilters`, and `sortFlags` can optionally be specified to exclude certain directories, filter - //! out certain types of entries, and sort the matches. - FileSearch(const QString& filter, const QStringList& paths, const QStringList& extensions, - const QStringList& excludedPaths = {}, QDir::Filters dirFilters = QDir::Filters{}, - QDir::SortFlags sortFlags = QDir::SortFlags{}); - - //! Execute the search, emitting the `foundResult` signal when matches are found. - void operator()(); - - //! Cancel the search. - void cancel(); - -signals: - //! Emitted when a result is found when searching the file system. - void foundMatch(FileSearch* search, const QString& match); - - //! Emitted when the search completes. - void searchCompleted(FileSearch* search); - -private: - static auto isPathExcluded(const QString& path) -> bool; - QString m_filter; - QStringList m_paths; - QStringList m_extensions; - QStringList m_excludedPaths; - QDir::Filters m_dirFilters; - QDir::SortFlags m_sortFlags; - std::atomic m_cancel = false; -}; -} // namespace lmms -#endif // LMMS_FILE_SEARCH_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1e2c4f3cfdb..5308537e2e9 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -23,7 +23,6 @@ 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 diff --git a/src/core/FileSearch.cpp b/src/core/FileSearch.cpp deleted file mode 100644 index 8a668360e2c..00000000000 --- a/src/core/FileSearch.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 -#include -#include - -#include - -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 diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index a9f18d6fdc5..b51874bff0e 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -39,14 +39,13 @@ #include #include #include -#include +#include #include "AudioEngine.h" #include "ConfigManager.h" #include "DataFile.h" #include "Engine.h" #include "FileBrowser.h" -#include "FileSearch.h" #include "GuiApplication.h" #include "ImportFilter.h" #include "Instrument.h" @@ -64,7 +63,6 @@ #include "Song.h" #include "StringPairDrag.h" #include "TextFloat.h" -#include "ThreadPool.h" #include "embed.h" namespace lmms::gui @@ -126,8 +124,7 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, addContentWidget(m_searchTreeWidget); m_searchIndicator = new QProgressBar(this); - m_searchIndicator->setMinimum(0); - m_searchIndicator->setMaximum(100); + m_searchIndicator->setRange(0, 1); addContentWidget(m_searchIndicator); // Whenever the FileBrowser has focus, Ctrl+F should direct focus to its filter box. @@ -198,128 +195,65 @@ void FileBrowser::restoreDirectoriesStates() expandItems(m_savedExpandedDirs); } -void FileBrowser::foundSearchMatch(FileSearch* search, const QString& match) +void FileBrowser::onSearch(const QString& filter) { - assert(search != nullptr); - if (m_currentSearch.get() != search) { return; } + static auto s_mostRecentTimestamp = 0; - auto basePath = QString{}; - for (const auto& path : m_directories.split('*')) + if (filter.isEmpty()) { - if (!match.startsWith(QDir{path}.absolutePath())) { continue; } - basePath = path; - break; + m_searchTreeWidget->hide(); + m_fileBrowserTreeWidget->show(); + m_searchIndicator->setRange(0, 1); + return; } - if (basePath.isEmpty()) { return; } + m_fileBrowserTreeWidget->hide(); + m_searchTreeWidget->clear(); + m_searchTreeWidget->show(); + m_searchIndicator->setRange(0, 0); - const auto baseDir = QDir{basePath}; - const auto matchInfo = QFileInfo{match}; - const auto matchRelativeToBasePath = baseDir.relativeFilePath(match); + 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; } - auto pathParts = QDir::cleanPath(matchRelativeToBasePath).split("/"); - auto currentItem = static_cast(nullptr); - auto currentDir = baseDir; + auto directoryFilters = QDir::AllEntries | QDir::NoDotAndDotDot; + if (m_showHiddenContent) { directoryFilters |= QDir::Hidden; } - for (const auto& pathPart : pathParts) + for (const auto& path : directories) { - auto childCount = currentItem ? currentItem->childCount() : m_searchTreeWidget->topLevelItemCount(); - auto childItem = static_cast(nullptr); + auto dirIt = QDirIterator{path, directoryFilters, QDirIterator::IteratorFlag::Subdirectories | QDirIterator::IteratorFlag::FollowSymlinks}; - for (int i = 0; i < childCount; ++i) + while (dirIt.hasNext()) { - auto item = currentItem ? currentItem->child(i) : m_searchTreeWidget->topLevelItem(i); - if (item->text(0) == pathPart) - { - childItem = item; - break; - } + s_mostRecentTimestamp = QDateTime::currentMSecsSinceEpoch(); + const auto searchTimestamp = s_mostRecentTimestamp; - } + QCoreApplication::processEvents(); - if (!childItem) - { - auto pathPartInfo = QFileInfo(currentDir, pathPart); - if (pathPartInfo.isDir()) + // If the timestamp changed, that means another search had already commenced + // when we were processing Qt events + if (searchTimestamp != s_mostRecentTimestamp) { break; } + + const auto fileInfo = QFileInfo{dirIt.next()}; + if (!fileInfo.fileName().contains(filter, Qt::CaseInsensitive)) { continue; } + + if (fileInfo.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; + const auto item = new Directory(fileInfo.fileName(), fileInfo.dir().path(), m_filter); + m_searchTreeWidget->addTopLevelItem(item); } - else + else if (fileInfo.isFile()) { - auto item = new FileItem(pathPart, currentDir.path()); - currentItem ? currentItem->addChild(item) : m_searchTreeWidget->addTopLevelItem(item); - childItem = item; + const auto item = new FileItem(fileInfo.fileName(), fileInfo.dir().path()); + m_searchTreeWidget->addTopLevelItem(item); } } - - currentItem = childItem; - if (!currentDir.cd(pathPart)) { break; } - } -} - -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()) - { - displaySearch(false); - return; } - 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(); - displaySearch(true); - - auto browserExtensions = m_filter; - const auto searchExtensions = browserExtensions.remove("*.").split(' '); - - auto search = std::make_shared( - 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)(); }); + m_searchIndicator->setRange(0, 1); } -void FileBrowser::displaySearch(bool on) -{ - if (on) - { - m_searchTreeWidget->show(); - m_fileBrowserTreeWidget->hide(); - m_searchIndicator->setMaximum(0); - return; - } - - m_searchTreeWidget->hide(); - m_fileBrowserTreeWidget->show(); - m_searchIndicator->setMaximum(100); -} - - void FileBrowser::reloadTree() { if (m_filterEdit->text().isEmpty()) @@ -1039,12 +973,11 @@ void FileBrowserTreeWidget::updateDirectory(QTreeWidgetItem * item ) } } -Directory::Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation) +Directory::Directory(const QString& filename, const QString& path, const QString& filter) : QTreeWidgetItem(QStringList(filename), TypeDirectoryItem) , m_directories(path) , m_filter(filter) , m_dirCount(0) - , m_disableEntryPopulation(disableEntryPopulation) { setIcon(0, !QDir{fullName()}.isReadable() ? m_folderLockedPixmap : m_folderPixmap); setChildIndicatorPolicy( QTreeWidgetItem::ShowIndicator ); @@ -1059,7 +992,7 @@ void Directory::update() } setIcon(0, m_folderOpenedPixmap); - if (!m_disableEntryPopulation && !childCount()) + if (!childCount()) { m_dirCount = 0; // for all paths leading here, add their items