diff --git a/include/FileBrowser.h b/include/FileBrowser.h index d02434c86..3d08b19db 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -27,8 +27,8 @@ #include #include -#include +#include "FileSearchJob.h" #include "embed.h" #include @@ -43,7 +43,6 @@ class QProgressBar; namespace lmms { -class FileSearch; class InstrumentTrack; class PlayHandle; class TrackContainer; @@ -53,6 +52,7 @@ namespace gui class FileBrowserTreeWidget; class FileItem; +class FileSearchJob; class FileBrowser : public SideBarWidget { @@ -77,20 +77,6 @@ public: ~FileBrowser() override = default; - static QStringList excludedPaths() - { - static auto s_excludedPaths = QStringList{ -#ifdef LMMS_BUILD_LINUX - "/bin", "/boot", "/dev", "/etc", "/proc", "/run", "/sbin", - "/sys" -#endif -#ifdef LMMS_BUILD_WIN32 - "C:\\Windows" -#endif - }; - return s_excludedPaths; - } - static QDir::Filters dirFilters() { return QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot | QDir::Hidden; } static QDir::SortFlags sortFlags() { return QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase; } @@ -107,10 +93,10 @@ private: 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 onSearchMatch(const QString& path); + void onSearchStarted(); + void onSearchFinished(); void addContentCheckBox(); @@ -120,8 +106,8 @@ private: QLineEdit * m_filterEdit; Type m_type; - std::shared_ptr m_currentSearch; QProgressBar* m_searchIndicator = nullptr; + FileSearchJob m_searchJob; QString m_directories; //!< Directories to search, split with '*' QString m_filter; //!< Filter as used in QDir::match() @@ -141,9 +127,6 @@ private: QString m_previousFilterValue; } ; - - - class FileBrowserTreeWidget : public QTreeWidget { Q_OBJECT @@ -200,16 +183,22 @@ private slots: void updateDirectory(QTreeWidgetItem* item); } ; - - -class Directory : public QTreeWidgetItem +class FileBrowserWidgetItem : public QTreeWidgetItem { public: - Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation = false); + FileBrowserWidgetItem(const QStringList& strings, int type, QTreeWidget* parent = nullptr); + virtual QString fullName(QString path = QString{}) const = 0; +}; + + +class Directory : public FileBrowserWidgetItem +{ +public: + Directory(const QString& filename, const QString& path, const QString& filter); void update(); - inline QString fullName( QString path = QString() ) + QString fullName(QString path = QString{}) const override { if( path.isEmpty() ) { @@ -247,13 +236,12 @@ private: QString m_filter; int m_dirCount; - bool m_disableEntryPopulation = false; } ; -class FileItem : public QTreeWidgetItem +class FileItem : public FileBrowserWidgetItem { public: enum class FileType @@ -282,7 +270,7 @@ public: const QString & path ); FileItem( const QString & name, const QString & path ); - QString fullName() const + QString fullName(QString path = QString{}) const override { return QFileInfo(m_path, text(0)).absoluteFilePath(); } diff --git a/include/FileSearch.h b/include/FileSearch.h deleted file mode 100644 index fd311c903..000000000 --- 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/include/FileSearchJob.h b/include/FileSearchJob.h new file mode 100644 index 000000000..ac158d900 --- /dev/null +++ b/include/FileSearchJob.h @@ -0,0 +1,78 @@ +/* + * FileSearchJob.h + * + * Copyright (c) 2025 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_GUI_FILE_SEARCH_JOB_H +#define LMMS_GUI_FILE_SEARCH_JOB_H + +#include +#include +#include +#include + +namespace lmms::gui { +//! The `FileSearchJob` class allows for searching for files on the filesystem. +//! Searching occurs on a background thread, and results are emitted as a Qt slot back to the user. +class FileSearchJob : public QObject +{ + Q_OBJECT +public: + //! Represents a search task to be carried out by the search job. + struct Task + { + QString filter; //! The filter to be tokenized. + QStringList paths; //! The list of paths to search recursively through. + QStringList extensions; //! The list of allowed extensions. + QFlags dirFilters; //! The directory filter flag. + }; + + //! Create a search job with the given @p parent (if any). + FileSearchJob(QObject* parent = nullptr); + + //! Stop processing and destroys the object. + ~FileSearchJob(); + + //! Commit to searching with the given @p task. + //! Cancels any previous search. + //! Callers can connect to the provided signals to interact with the search and its progress. + void search(Task task); + +signals: + //! Emitted when the search job has found a matching path. + void foundMatch(const QString& path); + + //! Emitted when the search job has started searching. + void started(); + + //! Emitted when the search job has finished searching. + void finished(); + +private: + Q_DISABLE_MOVE(FileSearchJob) + void runSearch(Task task); + std::future m_task; + std::atomic_flag m_stop = ATOMIC_FLAG_INIT; +}; +} // namespace lmms::gui + +#endif // LMMS_GUI_FILE_SEARCH_JOB_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 74e3bf1b5..85344487b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -24,7 +24,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 cc9d49af2..000000000 --- a/src/core/FileSearch.cpp +++ /dev/null @@ -1,90 +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 - -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/CMakeLists.txt b/src/gui/CMakeLists.txt index f1712053f..030eab0c9 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -15,6 +15,7 @@ SET(LMMS_SRCS gui/embed.cpp gui/FileBrowser.cpp gui/FileRevealer.cpp + gui/FileSearchJob.cpp gui/GuiApplication.cpp gui/LadspaControlView.cpp gui/LfoControllerDialog.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 69d30367b..f11dd9931 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -44,8 +44,8 @@ #include "ConfigManager.h" #include "DataFile.h" #include "Engine.h" +#include "FileBrowser.h" #include "FileRevealer.h" -#include "FileSearch.h" #include "GuiApplication.h" #include "ImportFilter.h" #include "Instrument.h" @@ -106,6 +106,9 @@ FileBrowser::FileBrowser(Type type, const QString& directories, const QString& f m_filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition); connect(m_filterEdit, &QLineEdit::textEdited, this, &FileBrowser::onSearch); + connect(&m_searchJob, &FileSearchJob::started, this, &FileBrowser::onSearchStarted, Qt::QueuedConnection); + connect(&m_searchJob, &FileSearchJob::finished, this, &FileBrowser::onSearchFinished, Qt::QueuedConnection); + connect(&m_searchJob, &FileSearchJob::foundMatch, this, &FileBrowser::onSearchMatch, Qt::QueuedConnection); auto reload_btn = new QPushButton(embed::getIconPixmap("reload"), QString(), searchWidget); reload_btn->setToolTip( tr( "Refresh list" ) ); @@ -125,8 +128,7 @@ FileBrowser::FileBrowser(Type type, const QString& directories, const QString& f 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. @@ -205,89 +207,12 @@ void FileBrowser::restoreDirectoriesStates() expandItems(m_savedExpandedDirs); } -void FileBrowser::foundSearchMatch(FileSearch* search, const QString& match) -{ - assert(search != nullptr); - if (m_currentSearch.get() != search) { return; } - - auto basePath = QString{}; - for (const auto& path : m_directories.split('*')) - { - if (!match.startsWith(QDir{path}.absolutePath())) { continue; } - basePath = path; - break; - } - - 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(nullptr); - auto currentDir = baseDir; - - for (const auto& pathPart : pathParts) - { - auto childCount = currentItem ? currentItem->childCount() : m_searchTreeWidget->topLevelItemCount(); - auto childItem = static_cast(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::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); + m_searchTreeWidget->hide(); + m_fileBrowserTreeWidget->show(); return; } @@ -296,36 +221,39 @@ void FileBrowser::onSearch(const QString& filter) if (m_showFactoryContent && !m_showFactoryContent->isChecked()) { directories.removeAll(m_factoryDir); } if (directories.isEmpty()) { return; } + auto directoryFilters = QDir::AllEntries | QDir::NoDotAndDotDot; + if (m_showHiddenContent) { directoryFilters |= QDir::Hidden; } + + const auto searchTask = FileSearchJob::Task{.filter = filter, + .paths = directories, + .extensions = FileItem::defaultFilters().split(" "), + .dirFilters = directoryFilters}; + 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_searchTreeWidget->show(); + m_fileBrowserTreeWidget->hide(); + m_searchJob.search(searchTask); } -void FileBrowser::displaySearch(bool on) +void FileBrowser::onSearchMatch(const QString& path) { - if (on) - { - m_searchTreeWidget->show(); - m_fileBrowserTreeWidget->hide(); - m_searchIndicator->setMaximum(0); - return; - } - - m_searchTreeWidget->hide(); - m_fileBrowserTreeWidget->show(); - m_searchIndicator->setMaximum(100); + const auto fileInfo = QFileInfo{path}; + auto item = static_cast(nullptr); + if (fileInfo.isDir()) { item = new Directory(fileInfo.fileName(), fileInfo.dir().path(), m_filter); } + else if (fileInfo.isFile()) { item = new FileItem(fileInfo.fileName(), fileInfo.dir().path()); } + + m_searchTreeWidget->addTopLevelItem(item); } +void FileBrowser::onSearchStarted() +{ + m_searchIndicator->setRange(0, 0); +} + +void FileBrowser::onSearchFinished() +{ + m_searchIndicator->setRange(0, 1); +} void FileBrowser::reloadTree() { @@ -431,8 +359,6 @@ void FileBrowser::giveFocusToFilter() void FileBrowser::addItems(const QString & path ) { - if (FileBrowser::excludedPaths().contains(path)) { return; } - if( m_dirsAsItems ) { m_fileBrowserTreeWidget->addTopLevelItem( new Directory( path, QString(), m_filter ) ); @@ -448,8 +374,6 @@ void FileBrowser::addItems(const QString & path ) QDir::LocaleAware | QDir::DirsFirst | QDir::Name | QDir::IgnoreCase); for (const auto& entry : entries) { - if (FileBrowser::excludedPaths().contains(entry.absoluteFilePath())) { continue; } - QString fileName = entry.fileName(); if (entry.isHidden() && m_showHiddenContent && !m_showHiddenContent->isChecked()) continue; if (entry.isDir()) @@ -665,19 +589,36 @@ void FileBrowserTreeWidget::contextMenuEvent(QContextMenuEvent* e) QString fileManager = tr("file manager"); #endif - QTreeWidgetItem* item = itemAt(e->pos()); - if (item == nullptr) { return; } // program hangs when right-clicking on empty space otherwise + auto item = dynamic_cast(itemAt(e->pos())); + if (item == nullptr) { return; } - QMenu contextMenu(this); + auto contextMenu = QMenu{this}; + + const auto fontMetrics = QFontMetrics{qApp->font()}; + const auto maxHeaderWidth = 50 * fontMetrics.averageCharWidth(); + const auto elidedPath = fontMetrics.elidedText(PathUtil::toShortestRelative(item->fullName()), Qt::TextElideMode::ElideMiddle, maxHeaderWidth); + + auto header = new QAction{elidedPath}; + header->setDisabled(true); + + contextMenu.addAction(header); + contextMenu.addSeparator(); switch (item->type()) { case TypeFileItem: { auto file = dynamic_cast(item); - const auto path = QFileInfo{file->fullName()}.absoluteFilePath(); contextMenu.addAction(QIcon(embed::getIconPixmap("folder")), tr("Show in %1").arg(fileManager), - [=] { FileRevealer::reveal(file->fullName()); }); + [file] { FileRevealer::reveal(file->fullName()); }); + + if (file->isTrack()) + { + contextMenu.addAction( + tr("Send to active instrument-track"), [file, this] { sendToActiveInstrumentTrack(file); }); + } + + const auto path = QFileInfo{file->fullName()}.absoluteFilePath(); if (ConfigManager::inst()->isFavoriteItem(file->fullName())) { @@ -693,19 +634,18 @@ void FileBrowserTreeWidget::contextMenuEvent(QContextMenuEvent* e) if (file->isTrack()) { contextMenu.addSeparator(); - contextMenu.addAction( - tr("Send to active instrument-track"), [=, this] { sendToActiveInstrumentTrack(file); }); + contextMenu.addAction(tr("Send to active instrument-track"), [&] { sendToActiveInstrumentTrack(file); }); } auto songEditorHeader = new QAction(tr("Song Editor"), nullptr); songEditorHeader->setDisabled(true); - contextMenu.addAction( songEditorHeader ); - contextMenu.addActions( getContextActions(file, true) ); + contextMenu.addAction(songEditorHeader); + contextMenu.addActions(getContextActions(file, true)); auto patternEditorHeader = new QAction(tr("Pattern Editor"), nullptr); patternEditorHeader->setDisabled(true); contextMenu.addAction(patternEditorHeader); - contextMenu.addActions( getContextActions(file, false) ); + contextMenu.addActions(getContextActions(file, false)); break; } case TypeDirectoryItem: { @@ -1108,12 +1048,16 @@ void FileBrowserTreeWidget::updateDirectory(QTreeWidgetItem * item ) } } -Directory::Directory(const QString& filename, const QString& path, const QString& filter, bool disableEntryPopulation) - : QTreeWidgetItem(QStringList(filename), TypeDirectoryItem) +FileBrowserWidgetItem::FileBrowserWidgetItem(const QStringList& strings, int type, QTreeWidget* parent) + : QTreeWidgetItem(parent, strings, type) +{ +} + +Directory::Directory(const QString& filename, const QString& path, const QString& filter) + : FileBrowserWidgetItem(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 ); @@ -1128,7 +1072,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 @@ -1161,8 +1105,6 @@ void Directory::update() bool Directory::addItems(const QString& path) { - if (FileBrowser::excludedPaths().contains(path)) { return false; } - QDir thisDir(path); if (!thisDir.isReadable()) { return false; } @@ -1172,8 +1114,6 @@ bool Directory::addItems(const QString& path) = thisDir.entryInfoList(m_filter.split(' '), FileBrowser::dirFilters(), FileBrowser::sortFlags()); for (const auto& entry : entries) { - if (FileBrowser::excludedPaths().contains(entry.absoluteFilePath())) { continue; } - QString fileName = entry.fileName(); if (entry.isDir()) { @@ -1194,24 +1134,17 @@ bool Directory::addItems(const QString& path) return childCount() > 0; } - - - -FileItem::FileItem(QTreeWidget * parent, const QString & name, - const QString & path ) : - QTreeWidgetItem( parent, QStringList( name) , TypeFileItem ), - m_path( path ) +FileItem::FileItem(QTreeWidget* parent, const QString& name, const QString& path) + : FileBrowserWidgetItem(QStringList{name}, TypeFileItem, parent) + , m_path(path) { determineFileType(); initPixmaps(); } - - - -FileItem::FileItem(const QString & name, const QString & path ) : - QTreeWidgetItem( QStringList( name ), TypeFileItem ), - m_path( path ) +FileItem::FileItem(const QString& name, const QString& path) + : FileBrowserWidgetItem(QStringList{name}, TypeFileItem) + , m_path(path) { determineFileType(); initPixmaps(); diff --git a/src/gui/FileSearchJob.cpp b/src/gui/FileSearchJob.cpp new file mode 100644 index 000000000..5c0f501a2 --- /dev/null +++ b/src/gui/FileSearchJob.cpp @@ -0,0 +1,103 @@ +/* + * FileSearchJob.cpp + * + * Copyright (c) 2025 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 "FileSearchJob.h" + +#include +#include + +#include "ThreadPool.h" + +namespace lmms::gui { +FileSearchJob::FileSearchJob(QObject* parent) + : QObject(parent) +{ +} + +FileSearchJob::~FileSearchJob() +{ + if (m_task.valid()) + { + m_stop.test_and_set(std::memory_order_acquire); + m_task.get(); + } +} + +void FileSearchJob::search(Task task) +{ + if (m_task.valid()) + { + m_stop.test_and_set(std::memory_order_acquire); + m_task.get(); + m_stop.clear(std::memory_order_release); + } + + const auto fn = [this, task = std::move(task)] { runSearch(std::move(task)); }; + m_task = ThreadPool::instance().enqueue(std::move(fn)); +} + +void FileSearchJob::runSearch(Task task) +{ + // RE expression that matches regular tokens, and tokens with double quotes around them + static auto s_tokenRe = QRegularExpression{R"(\"([^"]+)\"|(\S+))"}; + + auto tokensIt = s_tokenRe.globalMatch(task.filter); + auto tokens = QStringList{}; + + while (tokensIt.hasNext()) + { + const auto match = tokensIt.next(); + const auto quoted = match.captured(1); + const auto plain = match.captured(2); + + if (!quoted.isEmpty()) { tokens.push_back(quoted); } + if (!plain.isEmpty()) { tokens.push_back(plain); } + } + + emit started(); + + for (const auto& path : task.paths) + { + auto dirIt = QDirIterator{path, task.dirFilters, + QDirIterator::IteratorFlag::Subdirectories | QDirIterator::IteratorFlag::FollowSymlinks}; + + while (dirIt.hasNext() && !m_stop.test(std::memory_order_relaxed)) + { + const auto fileInfo = QFileInfo{dirIt.next()}; + const auto fileName = fileInfo.fileName(); + const auto containsToken = std::all_of(tokens.begin(), tokens.end(), + [&](const auto& token) { return fileName.contains(token, Qt::CaseInsensitive); }); + + const auto validDir = fileInfo.isDir() && containsToken; + const auto validFile = fileInfo.isFile() && containsToken + && task.extensions.contains(QString{"*.%1"}.arg(fileInfo.completeSuffix()), Qt::CaseInsensitive); + + if (validDir || validFile) { emit foundMatch(fileInfo.filePath()); } + } + } + + emit finished(); +} + +} // namespace lmms::gui