diff --git a/include/FileBrowser.h b/include/FileBrowser.h index eafb827da..02fec2719 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -28,6 +28,17 @@ #include #include #include + +#ifdef __MINGW32__ +#include +#include +#include +#else +#include +#include +#include +#endif + #if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) #include #endif @@ -72,6 +83,8 @@ public: ~FileBrowser() override = default; + static QDir::Filters dirFilters(); + private slots: void reloadTree(); void expandItems( QTreeWidgetItem * item=nullptr, QList expandedDirs = QList() ); @@ -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 m_cancel = false; + bool m_stopped = false; + bool m_run = false; + inline static std::unique_ptr s_instance = nullptr; +}; + @@ -274,6 +332,7 @@ public: QString extension(); static QString extension( const QString & file ); + static QString defaultFilters(); private: diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 1be344c98..776ac0810 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -23,8 +23,11 @@ * */ +#include "FileBrowser.h" + #include #include +#include #include #include #include @@ -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(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(); } + 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 diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index f867d86d9..145467ed7 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -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);