From d403a5414065784ff1459c4e6516ea50b3113bf2 Mon Sep 17 00:00:00 2001 From: Andrew Wiltshire <62200778+AW1534@users.noreply.github.com> Date: Mon, 28 Apr 2025 02:34:37 +0100 Subject: [PATCH] Add option to favorite items in the file browser (#7753) Added the ability to favorite items. This gets added to its own tab named "My Favorites". --------- Co-authored-by: Sotonye Atemie --- data/themes/classic/{hq_mode.png => star.png} | Bin data/themes/default/{hq_mode.png => star.png} | Bin include/ConfigManager.h | 8 ++ include/FileBrowser.h | 34 ++++--- include/MainWindow.h | 1 + include/PathUtil.h | 4 +- src/core/ConfigManager.cpp | 73 ++++++++++--- src/core/PathUtil.cpp | 16 ++- src/gui/FileBrowser.cpp | 96 ++++++++++++++---- src/gui/MainWindow.cpp | 47 ++++----- 10 files changed, 202 insertions(+), 77 deletions(-) rename data/themes/classic/{hq_mode.png => star.png} (100%) rename data/themes/default/{hq_mode.png => star.png} (100%) diff --git a/data/themes/classic/hq_mode.png b/data/themes/classic/star.png similarity index 100% rename from data/themes/classic/hq_mode.png rename to data/themes/classic/star.png diff --git a/data/themes/default/hq_mode.png b/data/themes/default/star.png similarity index 100% rename from data/themes/default/hq_mode.png rename to data/themes/default/star.png diff --git a/include/ConfigManager.h b/include/ConfigManager.h index 3cba834e1..bf98f5999 100644 --- a/include/ConfigManager.h +++ b/include/ConfigManager.h @@ -214,6 +214,8 @@ public: return m_recentlyOpenedProjects; } + const QStringList& favoriteItems() { return m_favoriteItems; } + QString localeDir() const { return m_dataDir + LOCALE_PATH; @@ -240,6 +242,10 @@ public: void addRecentlyOpenedProject(const QString & _file); + void addFavoriteItem(const QString& item); + void removeFavoriteItem(const QString& item); + bool isFavoriteItem(const QString& item); + QString value(const QString& cls, const QString& attribute, const QString& defaultVal = "") const; void setValue(const QString & cls, const QString & attribute, @@ -265,6 +271,7 @@ public: signals: void valueChanged( QString cls, QString attribute, QString value ); + void favoritesChanged(); private: static ConfigManager * s_instanceOfMe; @@ -299,6 +306,7 @@ private: QString m_version; unsigned int m_configVersion; QStringList m_recentlyOpenedProjects; + QStringList m_favoriteItems; using stringPairVector = std::vector>; using settingsMap = QMap; diff --git a/include/FileBrowser.h b/include/FileBrowser.h index 6c10ee763..71a878fc1 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -61,19 +61,22 @@ class FileBrowser : public SideBarWidget { Q_OBJECT public: + enum class Type + { + Normal, + Favorites + }; + /** - Create a file browser side bar widget - @param directories '*'-separated list of directories to search for. - If a directory of factory files should be in the list it - must be the last one (for the factory files delimiter to work) - @param filter Filter as used in QDir::match - @param recurse *to be documented* - */ - FileBrowser( const QString & directories, const QString & filter, - const QString & title, const QPixmap & pm, - QWidget * parent, bool dirs_as_items = false, - const QString& userDir = "", - const QString& factoryDir = ""); + Create a file browser side bar widget + @param directories '*'-separated list of directories to search for. + If a directory of factory files should be in the list it + must be the last one (for the factory files delimiter to work) + @param filter Filter as used in QDir::match + @param recurse *to be documented* + */ + FileBrowser(Type type, const QString& directories, const QString& filter, const QString& title, const QPixmap& pm, + QWidget* parent, bool dirs_as_items = false, const QString& userDir = "", const QString& factoryDir = ""); ~FileBrowser() override = default; @@ -90,6 +93,7 @@ public: }; 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; } @@ -101,7 +105,7 @@ private slots: private: void keyPressEvent( QKeyEvent * ke ) override; - void addItems( const QString & path ); + void addItems(const QString & path); void saveDirectoriesStates(); void restoreDirectoriesStates(); @@ -117,6 +121,7 @@ private: FileBrowserTreeWidget * m_searchTreeWidget; QLineEdit * m_filterEdit; + Type m_type; std::shared_ptr m_currentSearch; QProgressBar* m_searchIndicator = nullptr; @@ -216,8 +221,7 @@ public: { path += QDir::separator(); } - return( QDir::cleanPath( path + text( 0 ) ) + - QDir::separator() ); + return QDir::cleanPath(path + text(0)); } inline void addDirectory( const QString & dir ) diff --git a/include/MainWindow.h b/include/MainWindow.h index 1330de8c5..d2efd4ac0 100644 --- a/include/MainWindow.h +++ b/include/MainWindow.h @@ -46,6 +46,7 @@ class ConfigManager; namespace gui { +class FileBrowser; class PluginView; class SubWindow; class ToolButton; diff --git a/include/PathUtil.h b/include/PathUtil.h index 9b410d014..9bc71c1a1 100644 --- a/include/PathUtil.h +++ b/include/PathUtil.h @@ -31,8 +31,8 @@ namespace lmms::PathUtil { - enum class Base { Absolute, ProjectDir, FactorySample, UserSample, UserVST, Preset, - UserLADSPA, DefaultLADSPA, UserSoundfont, DefaultSoundfont, UserGIG, DefaultGIG, + enum class Base { Absolute, ProjectDir, FactoryProjects, FactorySample, UserSample, UserVST, Preset, + FactoryPresets, UserLADSPA, DefaultLADSPA, UserSoundfont, DefaultSoundfont, UserGIG, DefaultGIG, LocalDir }; //! Return the directory associated with a given base as a QString diff --git a/src/core/ConfigManager.cpp b/src/core/ConfigManager.cpp index d3c973020..bdf4d6c32 100644 --- a/src/core/ConfigManager.cpp +++ b/src/core/ConfigManager.cpp @@ -22,19 +22,19 @@ * */ +#include "ConfigManager.h" -#include -#include -#include +#include #include +#include +#include +#include #include #include -#include "ConfigManager.h" +#include "GuiApplication.h" #include "MainWindow.h" #include "ProjectVersion.h" -#include "GuiApplication.h" - #include "lmmsversion.h" namespace lmms @@ -299,9 +299,6 @@ void ConfigManager::setBackgroundPicFile(const QString & backgroundPicFile) m_backgroundPicFile = backgroundPicFile; } - - - void ConfigManager::createWorkingDir() { QDir().mkpath(m_workingDir); @@ -335,8 +332,27 @@ void ConfigManager::addRecentlyOpenedProject(const QString & file) } } +void ConfigManager::addFavoriteItem(const QString& item) +{ + m_favoriteItems.push_back(item); + saveConfigFile(); + emit favoritesChanged(); +} +void ConfigManager::removeFavoriteItem(const QString& item) +{ + m_favoriteItems.removeAll(item); + saveConfigFile(); + emit favoritesChanged(); +} +bool ConfigManager::isFavoriteItem(const QString& item) +{ + const auto& items = favoriteItems(); + const auto it = std::find_if(items.begin(), items.end(), + [&](const auto& favoriteItem) { return QFileInfo{item} == QFileInfo{favoriteItem}; }); + return it != items.end(); +} QString ConfigManager::value(const QString& cls, const QString& attribute, const QString& defaultVal) const { @@ -466,8 +482,20 @@ void ConfigManager::loadConfigFile(const QString & configFile) { if(n.isElement() && n.toElement().hasAttributes()) { - m_recentlyOpenedProjects << - n.toElement().attribute("path"); + m_recentlyOpenedProjects << n.toElement().attribute("path"); + } + n = n.nextSibling(); + } + } + else if (node.nodeName() == "favoriteitems") + { + m_favoriteItems.clear(); + QDomNode n = node.firstChild(); + while (!n.isNull()) + { + if (n.isElement() && n.toElement().hasAttributes()) + { + m_favoriteItems << n.toElement().attribute("path"); } n = n.nextSibling(); } @@ -571,6 +599,16 @@ void ConfigManager::loadConfigFile(const QString & configFile) { createWorkingDir(); } + + for (auto& file : m_recentlyOpenedProjects) + { + file = PathUtil::toAbsolute(file); + } + + for (auto& file : m_favoriteItems) + { + file = PathUtil::toAbsolute(file); + } } @@ -614,11 +652,22 @@ void ConfigManager::saveConfigFile() for (const auto& recentlyOpenedProject : m_recentlyOpenedProjects) { QDomElement n = doc.createElement("file"); - n.setAttribute("path", recentlyOpenedProject); + n.setAttribute("path", PathUtil::toShortestRelative(recentlyOpenedProject)); recent_files.appendChild(n); } lmms_config.appendChild(recent_files); + QDomElement favorite_items = doc.createElement("favoriteitems"); + + for (const auto& favoriteItem : m_favoriteItems) + { + QDomElement n = doc.createElement("item"); + n.setAttribute("path", PathUtil::toShortestRelative(favoriteItem)); + favorite_items.appendChild(n); + } + + lmms_config.appendChild(favorite_items); + QString xml = "\n" + doc.toString(2); QFile outfile(m_lmmsRcFile); diff --git a/src/core/PathUtil.cpp b/src/core/PathUtil.cpp index 03ec465a9..144c8e986 100644 --- a/src/core/PathUtil.cpp +++ b/src/core/PathUtil.cpp @@ -9,8 +9,8 @@ namespace lmms::PathUtil { - auto relativeBases = std::array{ Base::ProjectDir, Base::FactorySample, Base::UserSample, Base::UserVST, Base::Preset, - Base::UserLADSPA, Base::DefaultLADSPA, Base::UserSoundfont, Base::DefaultSoundfont, Base::UserGIG, Base::DefaultGIG, + auto relativeBases = std::array{ Base::ProjectDir, Base::FactoryProjects, Base::FactorySample, Base::UserSample, Base::UserVST, Base::Preset, + Base::FactoryPresets, Base::UserLADSPA, Base::DefaultLADSPA, Base::UserSoundfont, Base::DefaultSoundfont, Base::UserGIG, Base::DefaultGIG, Base::LocalDir }; QString baseLocation(const Base base, bool* error /* = nullptr*/) @@ -22,6 +22,11 @@ namespace lmms::PathUtil switch (base) { case Base::ProjectDir : loc = ConfigManager::inst()->userProjectsDir(); break; + case Base::FactoryProjects : + { + QDir fpd = QDir(ConfigManager::inst()->factoryProjectsDir()); + loc = fpd.absolutePath(); break; + } case Base::FactorySample : { QDir fsd = QDir(ConfigManager::inst()->factorySamplesDir()); @@ -30,6 +35,11 @@ namespace lmms::PathUtil case Base::UserSample : loc = ConfigManager::inst()->userSamplesDir(); break; case Base::UserVST : loc = ConfigManager::inst()->userVstDir(); break; case Base::Preset : loc = ConfigManager::inst()->userPresetsDir(); break; + case Base::FactoryPresets : + { + QDir fpd = QDir(ConfigManager::inst()->factoryPresetsDir()); + loc = fpd.absolutePath(); break; + } case Base::UserLADSPA : loc = ConfigManager::inst()->ladspaDir(); break; case Base::DefaultLADSPA : loc = ConfigManager::inst()->userLadspaDir(); break; case Base::UserSoundfont : loc = ConfigManager::inst()->sf2Dir(); break; @@ -70,10 +80,12 @@ namespace lmms::PathUtil switch (base) { case Base::ProjectDir : return QStringLiteral("userprojects:"); + case Base::FactoryProjects : return QStringLiteral("factoryprojects:"); case Base::FactorySample : return QStringLiteral("factorysample:"); case Base::UserSample : return QStringLiteral("usersample:"); case Base::UserVST : return QStringLiteral("uservst:"); case Base::Preset : return QStringLiteral("preset:"); + case Base::FactoryPresets : return QStringLiteral("factorypreset:"); case Base::UserLADSPA : return QStringLiteral("userladspa:"); case Base::DefaultLADSPA : return QStringLiteral("defaultladspa:"); case Base::UserSoundfont : return QStringLiteral("usersoundfont:"); diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 3ba634176..11e48c00f 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -25,6 +25,7 @@ #include "FileBrowser.h" +#include #include #include #include @@ -77,18 +78,15 @@ enum TreeWidgetItemTypes TypeDirectoryItem } ; - -FileBrowser::FileBrowser(const QString & directories, const QString & filter, - const QString & title, const QPixmap & pm, - QWidget * parent, bool dirs_as_items, - const QString& userDir, - const QString& factoryDir): - SideBarWidget( title, pm, parent ), - m_directories( directories ), - m_filter( filter ), - m_dirsAsItems( dirs_as_items ), - m_userDir(userDir), - m_factoryDir(factoryDir) +FileBrowser::FileBrowser(Type type, const QString& directories, const QString& filter, const QString& title, const QPixmap& pm, + QWidget* parent, bool dirs_as_items, const QString& userDir, const QString& factoryDir) + : SideBarWidget(title, pm, parent) + , m_type(type) + , m_directories(directories) + , m_filter(filter) + , m_dirsAsItems(dirs_as_items) + , m_userDir(userDir) + , m_factoryDir(factoryDir) { setWindowTitle( tr( "Browser" ) ); @@ -136,6 +134,14 @@ FileBrowser::FileBrowser(const QString & directories, const QString & filter, m_previousFilterValue = ""; + if (m_type == Type::Favorites) + { + connect(ConfigManager::inst(), &ConfigManager::favoritesChanged, [this] { + m_directories = ConfigManager::inst()->favoriteItems().join("*"); + reloadTree(); + }); + } + reloadTree(); show(); } @@ -329,7 +335,7 @@ void FileBrowser::reloadTree() m_fileBrowserTreeWidget->clear(); - QStringList paths = m_directories.split('*'); + auto paths = m_directories.isEmpty() ? QStringList{} : m_directories.split('*'); if (m_showUserContent && !m_showUserContent->isChecked()) { @@ -341,12 +347,37 @@ void FileBrowser::reloadTree() paths.removeAll(m_factoryDir); } - if (!paths.isEmpty()) + switch (m_type) { + case Type::Favorites: + for (auto& path : paths) + { + while (path.endsWith('/') || path.endsWith('\\') || path.endsWith(".")) + { + path.chop(1); + } + + auto info = QFileInfo{PathUtil::toAbsolute(path)}; + + if (info.isDir()) + { + auto dir = new Directory(info.fileName(), info.absolutePath(), m_filter); + dir->update(); + m_fileBrowserTreeWidget->addTopLevelItem(dir); + } + else if (info.isFile()) + { + auto file = new FileItem(info.fileName(), info.path()); + m_fileBrowserTreeWidget->addTopLevelItem(file); + } + } + break; + case Type::Normal: for (const auto& path : paths) { addItems(path); } + break; } if (m_filterEdit->text().isEmpty()) @@ -642,17 +673,29 @@ void FileBrowserTreeWidget::contextMenuEvent(QContextMenuEvent* e) { case TypeFileItem: { auto file = dynamic_cast(item); - - if (file->isTrack()) - { - contextMenu.addAction( - tr("Send to active instrument-track"), [=, this] { sendToActiveInstrumentTrack(file); }); - contextMenu.addSeparator(); - } + const auto path = QFileInfo{file->fullName()}.absoluteFilePath(); contextMenu.addAction(QIcon(embed::getIconPixmap("folder")), tr("Show in %1").arg(fileManager), [=] { FileRevealer::reveal(file->fullName()); }); + if (ConfigManager::inst()->isFavoriteItem(file->fullName())) + { + contextMenu.addAction( + QIcon(embed::getIconPixmap("star")), tr("Remove favorite file"), [path] { ConfigManager::inst()->removeFavoriteItem(path); }); + } + else + { + contextMenu.addAction( + QIcon(embed::getIconPixmap("star")), tr("Add favorite file"), [path] { ConfigManager::inst()->addFavoriteItem(path); }); + } + + if (file->isTrack()) + { + contextMenu.addSeparator(); + contextMenu.addAction( + tr("Send to active instrument-track"), [=, this] { sendToActiveInstrumentTrack(file); }); + } + auto songEditorHeader = new QAction(tr("Song Editor"), nullptr); songEditorHeader->setDisabled(true); contextMenu.addAction( songEditorHeader ); @@ -666,9 +709,20 @@ void FileBrowserTreeWidget::contextMenuEvent(QContextMenuEvent* e) } case TypeDirectoryItem: { auto dir = dynamic_cast(item); + const auto path = QFileInfo{dir->fullName()}.absoluteFilePath(); + contextMenu.addAction(QIcon(embed::getIconPixmap("folder")), tr("Open in %1").arg(fileManager), [=] { FileRevealer::openDir(dir->fullName()); }); + + if (ConfigManager::inst()->isFavoriteItem(dir->fullName())) + { + contextMenu.addAction(QIcon(embed::getIconPixmap("star")), tr("Remove favorite folder"), [path] { ConfigManager::inst()->removeFavoriteItem(path); }); + } + else + { + contextMenu.addAction(QIcon(embed::getIconPixmap("star")), tr("Add favorite folder"), [path] { ConfigManager::inst()->addFavoriteItem(path); }); + } break; } } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index b012eca7a..90d4196a3 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -111,30 +111,27 @@ MainWindow::MainWindow() : emit initProgress(tr("Preparing plugin browser")); sideBar->appendTab( new PluginBrowser( splitter ) ); emit initProgress(tr("Preparing file browsers")); - sideBar->appendTab( new FileBrowser( - confMgr->userProjectsDir() + "*" + - confMgr->factoryProjectsDir(), - "*.mmp *.mmpz *.xml *.mid *.mpt", - tr( "My Projects" ), - embed::getIconPixmap( "project_file" ).transformed( QTransform().rotate( 90 ) ), - splitter, false, - confMgr->userProjectsDir(), - confMgr->factoryProjectsDir())); - sideBar->appendTab( - new FileBrowser(confMgr->userSamplesDir() + "*" + confMgr->factorySamplesDir(), FileItem::defaultFilters(), - tr("My Samples"), embed::getIconPixmap("sample_file").transformed(QTransform().rotate(90)), splitter, false, - confMgr->userSamplesDir(), confMgr->factorySamplesDir())); - sideBar->appendTab( new FileBrowser( - confMgr->userPresetsDir() + "*" + - confMgr->factoryPresetsDir(), - "*.xpf *.cs.xml *.xiz *.lv2", - tr( "My Presets" ), - embed::getIconPixmap( "preset_file" ).transformed( QTransform().rotate( 90 ) ), - splitter , false, - confMgr->userPresetsDir(), - confMgr->factoryPresetsDir())); - sideBar->appendTab(new FileBrowser(QDir::homePath(), FileItem::defaultFilters(), tr("My Home"), - embed::getIconPixmap("home").transformed(QTransform().rotate(90)), splitter, false)); + + sideBar->appendTab(new FileBrowser(FileBrowser::Type::Favorites, ConfigManager::inst()->favoriteItems().join("*"), FileItem::defaultFilters(), "My Favorites", + embed::getIconPixmap("star").transformed(QTransform().rotate(90)), splitter, false, "", "")); + + sideBar->appendTab(new FileBrowser(FileBrowser::Type::Normal, + confMgr->userProjectsDir() + "*" + confMgr->factoryProjectsDir(), "*.mmp *.mmpz *.xml *.mid *.mpt", + tr("My Projects"), embed::getIconPixmap("project_file").transformed(QTransform().rotate(90)), splitter, false, + confMgr->userProjectsDir(), confMgr->factoryProjectsDir())); + + sideBar->appendTab(new FileBrowser(FileBrowser::Type::Normal, + confMgr->userSamplesDir() + "*" + confMgr->factorySamplesDir(), FileItem::defaultFilters(), tr("My Samples"), + embed::getIconPixmap("sample_file").transformed(QTransform().rotate(90)), splitter, false, + confMgr->userSamplesDir(), confMgr->factorySamplesDir())); + + sideBar->appendTab(new FileBrowser(FileBrowser::Type::Normal, + confMgr->userPresetsDir() + "*" + confMgr->factoryPresetsDir(), "*.xpf *.cs.xml *.xiz *.lv2", tr("My Presets"), + embed::getIconPixmap("preset_file").transformed(QTransform().rotate(90)), splitter, false, + confMgr->userPresetsDir(), confMgr->factoryPresetsDir())); + + sideBar->appendTab(new FileBrowser(FileBrowser::Type::Normal, QDir::homePath(), FileItem::defaultFilters(), + tr("My Home"), embed::getIconPixmap("home").transformed(QTransform().rotate(90)), splitter, false)); QStringList root_paths; QString title = tr("Root Directory"); @@ -156,7 +153,7 @@ MainWindow::MainWindow() : } #endif - sideBar->appendTab(new FileBrowser(root_paths.join("*"), FileItem::defaultFilters(), title, + sideBar->appendTab(new FileBrowser(FileBrowser::Type::Normal, root_paths.join("*"), FileItem::defaultFilters(), title, embed::getIconPixmap("computer").transformed(QTransform().rotate(90)), splitter, dirs_as_items)); m_workspace = new MovableQMdiArea(splitter);