diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index e4157ea2d..56ff10e51 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -56,7 +56,7 @@ IconList::IconList(const QStringList& builtinPaths, QString path, QObject* paren QDir instance_icons(builtinPath); auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); for (auto file_info : file_info_list) { - builtinNames.insert(file_info.completeBaseName()); + builtinNames.insert(file_info.baseName()); } } for (auto& builtinName : builtinNames) { @@ -77,10 +77,95 @@ IconList::IconList(const QStringList& builtinPaths, QString path, QObject* paren void IconList::sortIconList() { qDebug() << "Sorting icon list..."; - std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { return a.m_key.localeAwareCompare(b.m_key) < 0; }); + std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { + bool aIsSubdir = a.m_key.contains(std::filesystem::path::preferred_separator); + bool bIsSubdir = b.m_key.contains(std::filesystem::path::preferred_separator); + if (aIsSubdir != bIsSubdir) { + return !aIsSubdir; // root-level icons come first + } + return a.m_key.localeAwareCompare(b.m_key) < 0; + }); reindex(); } +// Helper function to add directories recursively +bool IconList::addPathRecursively(const QString& path) +{ + QDir dir(path); + if (!dir.exists()) + return false; + + bool watching = false; + + // Add the directory itself + watching = m_watcher->addPath(path); + + // Add all subdirectories + QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo& entry : entries) + { + if (addPathRecursively(entry.absoluteFilePath())) + { + watching = true; + } + } + return watching; +} + +void IconList::removePathRecursively(const QString& path) +{ + QFileInfo file_info(path); + if (file_info.isFile()) { + // Remove the icon belonging to the file + QString key = m_dir.relativeFilePath(file_info.absoluteFilePath()); + int idx = getIconIndex(key); + if (idx == -1) + return; + + } + else if (file_info.isDir()) { + // Remove the directory itself + m_watcher->removePath(path); + + const QDir dir(path); + // Remove all files within the directory + for (const QFileInfo& file : dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) + { + removePathRecursively(file.absoluteFilePath()); + } + } +} + +QStringList IconList::getIconFilePaths() const +{ + QStringList icon_files {}; + QStringList directories {m_dir.absolutePath()}; + while (!directories.isEmpty()) { + QString first = directories.takeFirst(); + QDir dir(first); + for (QString& file_name : dir.entryList(QDir::AllDirs | QDir::Files |QDir::NoDotAndDotDot, QDir::Name)) { + QString full_path = dir.filePath(file_name); // Convert to full path + QFileInfo file_info(full_path); + if (file_info.isDir()) + directories.push_back(full_path); + else + icon_files.push_back(full_path); + } + } + return icon_files; +} + +QString formatName(const QDir& icons_dir, const QFileInfo& file) +{ + if (file.dir() == icons_dir) { + return file.baseName(); + } + else { + const QString delimiter = " ยป "; + return (icons_dir.relativeFilePath(file.dir().path()) + std::filesystem::path::preferred_separator + file.baseName()).replace(std::filesystem::path::preferred_separator, delimiter); + } +} + void IconList::directoryChanged(const QString& path) { QDir new_dir(path); @@ -95,13 +180,9 @@ void IconList::directoryChanged(const QString& path) if (!FS::ensureFolderPathExists(m_dir.absolutePath())) return; m_dir.refresh(); - auto new_list = m_dir.entryList(QDir::Files, QDir::Name); - for (auto it = new_list.begin(); it != new_list.end(); it++) { - QString& foo = (*it); - foo = m_dir.filePath(foo); - } + QStringList new_file_names_list = getIconFilePaths(); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QSet new_set(new_list.begin(), new_list.end()); + QSet new_set(new_file_names_list.begin(), new_file_names_list.end()); #else auto new_set = new_list.toSet(); #endif @@ -123,21 +204,16 @@ void IconList::directoryChanged(const QString& path) QSet to_add = new_set; to_add -= current_set; - for (auto remove : to_remove) { + for (const auto& remove : to_remove) { qDebug() << "Removing " << remove; - QFileInfo rmfile(remove); - QString key = rmfile.completeBaseName(); - - QString suffix = rmfile.suffix(); - // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well - if (!IconUtils::isIconSuffix(suffix)) - key = rmfile.fileName(); + QFileInfo removed_file(remove); + QString key = m_dir.relativeFilePath(removed_file.absoluteFilePath()); int idx = getIconIndex(key); if (idx == -1) continue; - icons[idx].remove(IconType::FileBased); - if (icons[idx].type() == IconType::ToBeDeleted) { + icons[idx].remove(FileBased); + if (icons[idx].type() == ToBeDeleted) { beginRemoveRows(QModelIndex(), idx, idx); icons.remove(idx); reindex(); @@ -149,18 +225,14 @@ void IconList::directoryChanged(const QString& path) emit iconUpdated(key); } - for (auto add : to_add) { + for (const auto& add : to_add) { qDebug() << "Adding " << add; QFileInfo addfile(add); - QString key = addfile.completeBaseName(); + QString key = m_dir.relativeFilePath(addfile.absoluteFilePath()); + QString name = formatName(m_dir, addfile); - QString suffix = addfile.suffix(); - // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well - if (!IconUtils::isIconSuffix(suffix)) - key = addfile.fileName(); - - if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) { + if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) { m_watcher->addPath(add); emit iconUpdated(key); } @@ -175,7 +247,7 @@ void IconList::fileChanged(const QString& path) QFileInfo checkfile(path); if (!checkfile.exists()) return; - QString key = checkfile.completeBaseName(); + QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath()); int idx = getIconIndex(key); if (idx == -1) return; @@ -200,7 +272,7 @@ void IconList::startWatching() { auto abs_path = m_dir.absolutePath(); FS::ensureFolderPathExists(abs_path); - is_watching = m_watcher->addPath(abs_path); + is_watching = addPathRecursively(abs_path); if (is_watching) { qDebug() << "Started watching " << abs_path; } else { @@ -312,6 +384,7 @@ bool IconList::iconFileExists(const QString& key) const return iconEntry && iconEntry->has(IconType::FileBased); } +/// Returns the icon with the given key or nullptr if it doesn't exist. const MMCIcon* IconList::icon(const QString& key) const { int iconIdx = getIconIndex(key); @@ -394,6 +467,7 @@ void IconList::reindex() for (auto& iter : icons) { name_index[iter.m_key] = i; i++; + emit iconUpdated(iter.m_key); // prevents incorrect indices with proxy model } } @@ -425,3 +499,15 @@ QString IconList::getDirectory() const { return m_dir.absolutePath(); } + +/// Returns the directory of the icon with the given key or the default directory if it's a builtin icon. +QString IconList::iconDirectory(const QString& key) const +{ + for (auto mmc_icon : icons) { + if (mmc_icon.m_key == key && mmc_icon.has(IconType::FileBased)) { + QFileInfo icon_file(mmc_icon.getFilePath()); + return icon_file.dir().path(); + } + } + return getDirectory(); +} diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index 553946c42..fcd172561 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -72,6 +72,7 @@ class IconList : public QAbstractListModel { bool deleteIcon(const QString& key); bool trashIcon(const QString& key); bool iconFileExists(const QString& key) const; + QString iconDirectory(const QString& key) const; void installIcons(const QStringList& iconFiles); void installIcon(const QString& file, const QString& name); @@ -91,6 +92,9 @@ class IconList : public QAbstractListModel { IconList& operator=(const IconList&) = delete; void reindex(); void sortIconList(); + bool addPathRecursively(const QString& path); + void removePathRecursively(const QString& path); + QStringList getIconFilePaths() const; public slots: void directoryChanged(const QString& path); diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index a196fd587..a3f1d7ea4 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include "Application.h" @@ -33,6 +35,15 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui ui->setupUi(this); setWindowModality(Qt::WindowModal); + searchBar = new QLineEdit(this); + searchBar->setPlaceholderText(tr("Search...")); + ui->verticalLayout->insertWidget(0, searchBar); + + proxyModel = new QSortFilterProxyModel(this); + proxyModel->setSourceModel(APPLICATION->icons().get()); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + ui->iconView->setModel(proxyModel); + auto contentsWidget = ui->iconView; contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setFlow(QListView::LeftToRight); @@ -57,7 +68,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui contentsWidget->installEventFilter(this); - contentsWidget->setModel(APPLICATION->icons().get()); + contentsWidget->setModel(proxyModel); // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win. auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); @@ -73,6 +84,9 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); + connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons); + // Prevent incorrect indices from e.g. filesystem changes + connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); }); } bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt) @@ -159,5 +173,10 @@ IconPickerDialog::~IconPickerDialog() void IconPickerDialog::openFolder() { - DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true); + DesktopServices::openPath(APPLICATION->icons()->iconDirectory(selectedIconKey), true); } + +void IconPickerDialog::filterIcons(const QString& query) +{ + proxyModel->setFilterFixedString(query); +} \ No newline at end of file diff --git a/launcher/ui/dialogs/IconPickerDialog.h b/launcher/ui/dialogs/IconPickerDialog.h index 37e53dcce..db1315338 100644 --- a/launcher/ui/dialogs/IconPickerDialog.h +++ b/launcher/ui/dialogs/IconPickerDialog.h @@ -16,6 +16,8 @@ #pragma once #include #include +#include +#include namespace Ui { class IconPickerDialog; @@ -36,6 +38,8 @@ class IconPickerDialog : public QDialog { private: Ui::IconPickerDialog* ui; QPushButton* buttonRemove; + QLineEdit* searchBar; + QSortFilterProxyModel* proxyModel; private slots: void selectionChanged(QItemSelection, QItemSelection); @@ -44,4 +48,5 @@ class IconPickerDialog : public QDialog { void addNewIcon(); void removeSelectedIcon(); void openFolder(); + void filterIcons(const QString& text); };