diff --git a/dist/license.md b/dist/license.md
index b3e8d05a4..207fc638e 100644
--- a/dist/license.md
+++ b/dist/license.md
@@ -15,6 +15,7 @@ qt_themes/default/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/default/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/default/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/16x16/disconnected.png | CC BY-ND 3.0 | https://icons8.com
@@ -26,6 +27,7 @@ qt_themes/qdarkstyle/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/qdarkstyle/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
qt_themes/qdarkstyle/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/qdarkstyle/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/16x16/disconnected.png | CC BY-ND 3.0 | https://icons8.com
@@ -36,6 +38,7 @@ qt_themes/colorful/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/48x48/plus.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/colorful/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com
qt_themes/colorful_dark/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com
diff --git a/dist/qt_themes/colorful/icons/48x48/star.png b/dist/qt_themes/colorful/icons/48x48/star.png
new file mode 100644
index 000000000..19d55a0a8
Binary files /dev/null and b/dist/qt_themes/colorful/icons/48x48/star.png differ
diff --git a/dist/qt_themes/colorful/style.qrc b/dist/qt_themes/colorful/style.qrc
index 2866c69bd..bd7898eb1 100644
--- a/dist/qt_themes/colorful/style.qrc
+++ b/dist/qt_themes/colorful/style.qrc
@@ -10,6 +10,7 @@
icons/48x48/folder.png
icons/48x48/plus.png
icons/48x48/sd_card.png
+ icons/48x48/star.png
icons/256x256/plus_folder.png
diff --git a/dist/qt_themes/colorful_dark/style.qrc b/dist/qt_themes/colorful_dark/style.qrc
index 9c531fe1b..ec328117d 100644
--- a/dist/qt_themes/colorful_dark/style.qrc
+++ b/dist/qt_themes/colorful_dark/style.qrc
@@ -11,6 +11,7 @@
../qdarkstyle/icons/48x48/no_avatar.png
../colorful/icons/48x48/plus.png
../colorful/icons/48x48/sd_card.png
+ ../colorful/icons/48x48/star.png
../colorful/icons/256x256/plus_folder.png
diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc
index 6da475316..3c8a901b5 100644
--- a/dist/qt_themes/default/default.qrc
+++ b/dist/qt_themes/default/default.qrc
@@ -13,6 +13,7 @@
icons/48x48/no_avatar.png
icons/48x48/plus.png
icons/48x48/sd_card.png
+ icons/48x48/star.png
icons/256x256/citra.png
icons/256x256/plus_folder.png
diff --git a/dist/qt_themes/default/icons/48x48/star.png b/dist/qt_themes/default/icons/48x48/star.png
new file mode 100644
index 000000000..c2b78f0c3
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/star.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/star.png b/dist/qt_themes/qdarkstyle/icons/48x48/star.png
new file mode 100644
index 000000000..546779e2a
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/star.png differ
diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc
index 0a4424b7f..84360a65c 100644
--- a/dist/qt_themes/qdarkstyle/style.qrc
+++ b/dist/qt_themes/qdarkstyle/style.qrc
@@ -11,6 +11,7 @@
icons/48x48/no_avatar.png
icons/48x48/plus.png
icons/48x48/sd_card.png
+ icons/48x48/star.png
icons/256x256/plus_folder.png
diff --git a/dist/qt_themes/qdarkstyle_midnight_blue/icons/48x48/star.png b/dist/qt_themes/qdarkstyle_midnight_blue/icons/48x48/star.png
new file mode 100644
index 000000000..90d423a1d
Binary files /dev/null and b/dist/qt_themes/qdarkstyle_midnight_blue/icons/48x48/star.png differ
diff --git a/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc b/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc
index b55a72fd1..3b08654f8 100644
--- a/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc
+++ b/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc
@@ -11,6 +11,7 @@
icons/48x48/no_avatar.png
icons/48x48/plus.png
icons/48x48/sd_card.png
+ icons/48x48/star.png
icons/256x256/plus_folder.png
diff --git a/license.txt b/license.txt
index 09f9ad2d5..f94c73f1a 100644
--- a/license.txt
+++ b/license.txt
@@ -356,3 +356,4 @@ folder.png | CC BY-ND 3.0 | https://icons8.com
plus.png (Default, Dark) | CC0 1.0 | Designed by BreadFish64 from the Citra team
plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com
sd_card.png | CC BY-ND 3.0 | https://icons8.com
+star.png | CC BY-ND 3.0 | https://icons8.com
diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp
index 389a3becf..ab2a65752 100644
--- a/src/citra_qt/configuration/config.cpp
+++ b/src/citra_qt/configuration/config.cpp
@@ -791,6 +791,14 @@ void Config::ReadUIGameListValues() {
ReadBasicSetting(UISettings::values.show_type_column);
ReadBasicSetting(UISettings::values.show_size_column);
+ const int favorites_size = qt_config->beginReadArray(QStringLiteral("favorites"));
+ for (int i = 0; i < favorites_size; i++) {
+ qt_config->setArrayIndex(i);
+ UISettings::values.favorited_ids.append(
+ ReadSetting(QStringLiteral("program_id")).toULongLong());
+ }
+ qt_config->endArray();
+
qt_config->endGroup();
}
@@ -1265,6 +1273,14 @@ void Config::SaveUIGameListValues() {
WriteBasicSetting(UISettings::values.show_type_column);
WriteBasicSetting(UISettings::values.show_size_column);
+ qt_config->beginWriteArray(QStringLiteral("favorites"));
+ for (int i = 0; i < UISettings::values.favorited_ids.size(); i++) {
+ qt_config->setArrayIndex(i);
+ WriteSetting(QStringLiteral("program_id"),
+ QVariant::fromValue(UISettings::values.favorited_ids[i]));
+ }
+ qt_config->endArray();
+
qt_config->endGroup();
}
diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp
index d3d4e20d1..9f520739c 100644
--- a/src/citra_qt/game_list.cpp
+++ b/src/citra_qt/game_list.cpp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -105,6 +106,10 @@ void GameListSearchField::setFilterResult(int visible, int total) {
QStringLiteral("%1 %2 %3 %4").arg(visible).arg(result_of_text).arg(total).arg(result_text));
}
+bool GameListSearchField::IsEmpty() const {
+ return edit_filter->text().isEmpty();
+}
+
QString GameList::GetLastFilterResultItem() const {
QString file_path;
const int folderCount = item_model->rowCount();
@@ -206,7 +211,9 @@ void GameList::OnTextChanged(const QString& new_text) {
// If the searchfield is empty every item is visible
// Otherwise the filter gets applied
if (edit_filter_text.isEmpty()) {
- for (int i = 0; i < folder_count; ++i) {
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
+ UISettings::values.favorited_ids.size() == 0);
+ for (int i = 1; i < folder_count; ++i) {
folder = item_model->item(i, 0);
const QModelIndex folder_index = folder->index();
const int children_count = folder->rowCount();
@@ -217,8 +224,9 @@ void GameList::OnTextChanged(const QString& new_text) {
}
search_field->setFilterResult(children_total, children_total);
} else {
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
int result_count = 0;
- for (int i = 0; i < folder_count; ++i) {
+ for (int i = 1; i < folder_count; ++i) {
folder = item_model->item(i, 0);
const QModelIndex folder_index = folder->index();
const int children_count = folder->rowCount();
@@ -281,6 +289,13 @@ void GameList::OnUpdateThemedIcons() {
child->setData(QIcon::fromTheme(QStringLiteral("plus")).pixmap(icon_size),
Qt::DecorationRole);
break;
+ case GameListItemType::Favorites:
+ child->setData(
+ QIcon::fromTheme(QStringLiteral("star"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ break;
default:
break;
}
@@ -422,6 +437,15 @@ void GameList::DonePopulating(const QStringList& watch_list) {
item_model->invisibleRootItem()->appendRow(new GameListAddDir());
+ // Add favorites row
+ item_model->invisibleRootItem()->insertRow(0, new GameListFavorites());
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
+ UISettings::values.favorited_ids.size() == 0);
+ tree_view->expand(item_model->invisibleRootItem()->child(0)->index());
+ for (const auto id : UISettings::values.favorited_ids) {
+ AddFavorite(id);
+ }
+
// Clear out the old directories to watch for changes and add the new ones
auto watch_dirs = watcher->directories();
if (!watch_dirs.isEmpty()) {
@@ -440,7 +464,7 @@ void GameList::DonePopulating(const QStringList& watch_list) {
tree_view->setEnabled(true);
const int folderCount = tree_view->model()->rowCount();
int children_total = 0;
- for (int i = 0; i < folderCount; ++i) {
+ for (int i = 1; i < folderCount; ++i) {
children_total += item_model->item(i, 0)->rowCount();
}
search_field->setFilterResult(children_total, children_total);
@@ -477,6 +501,9 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
case GameListItemType::SystemDir:
AddPermDirPopup(context_menu, selected);
break;
+ case GameListItemType::Favorites:
+ AddFavoritesPopup(context_menu);
+ break;
default:
break;
}
@@ -533,6 +560,8 @@ void ForEachOpenGLCacheFile(u64 program_id, auto func) {
void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QString& name,
u64 program_id, u64 extdata_id, Service::FS::MediaType media_type) {
+ QAction* favorite = context_menu.addAction(tr("Favorite"));
+ context_menu.addSeparator();
QMenu* open_menu = context_menu.addMenu(tr("Open"));
QAction* open_application_location = open_menu->addAction(tr("Application Location"));
open_menu->addSeparator();
@@ -577,6 +606,10 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr
program_id, [&opengl_cache_exists](QFile& file) { opengl_cache_exists |= file.exists(); });
#endif
+ favorite->setVisible(program_id != 0);
+ favorite->setCheckable(true);
+ favorite->setChecked(UISettings::values.favorited_ids.contains(program_id));
+
std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir);
open_save_location->setEnabled(
is_application && FileUtil::Exists(FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(
@@ -624,6 +657,7 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
navigate_to_gamedb_entry->setVisible(it != compatibility_list.end());
+ connect(favorite, &QAction::triggered, [this, program_id]() { ToggleFavorite(program_id); });
connect(open_save_location, &QAction::triggered, this, [this, program_id] {
emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA);
});
@@ -774,7 +808,7 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
const int row = selected.row();
- move_up->setEnabled(row > 0);
+ move_up->setEnabled(row > 1);
move_down->setEnabled(row < item_model->rowCount() - 2);
connect(move_up, &QAction::triggered, this, [this, selected, row, game_dir_index] {
@@ -812,6 +846,18 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
});
}
+void GameList::AddFavoritesPopup(QMenu& context_menu) {
+ QAction* clear = context_menu.addAction(tr("Clear"));
+
+ connect(clear, &QAction::triggered, [this] {
+ for (const auto id : UISettings::values.favorited_ids) {
+ RemoveFavorite(id);
+ }
+ UISettings::values.favorited_ids.clear();
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
+ });
+}
+
void GameList::LoadCompatibilityList() {
QFile compat_list{QStringLiteral(":compatibility_list/compatibility_list.json")};
@@ -944,6 +990,58 @@ void GameList::RefreshGameDirectory() {
}
}
+void GameList::ToggleFavorite(u64 program_id) {
+ if (!UISettings::values.favorited_ids.contains(program_id)) {
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
+ !search_field->IsEmpty());
+ UISettings::values.favorited_ids.append(program_id);
+ AddFavorite(program_id);
+ item_model->sort(tree_view->header()->sortIndicatorSection(),
+ tree_view->header()->sortIndicatorOrder());
+ } else {
+ UISettings::values.favorited_ids.removeOne(program_id);
+ RemoveFavorite(program_id);
+ if (UISettings::values.favorited_ids.size() == 0) {
+ tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
+ }
+ }
+}
+
+void GameList::AddFavorite(u64 program_id) {
+ auto* favorites_row = item_model->item(0);
+
+ for (int i = 1; i < item_model->rowCount() - 1; i++) {
+ const auto* folder = item_model->item(i);
+ for (int j = 0; j < folder->rowCount(); j++) {
+ if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() ==
+ program_id) {
+ QList list;
+ for (int k = 0; k < COLUMN_COUNT; k++) {
+ list.append(folder->child(j, k)->clone());
+ }
+ list[0]->setData(folder->child(j)->data(GameListItem::SortRole),
+ GameListItem::SortRole);
+ list[0]->setText(folder->child(j)->data(Qt::DisplayRole).toString());
+
+ favorites_row->appendRow(list);
+ return;
+ }
+ }
+ }
+}
+
+void GameList::RemoveFavorite(u64 program_id) {
+ auto* favorites_row = item_model->item(0);
+
+ for (int i = 0; i < favorites_row->rowCount(); i++) {
+ const auto* game = favorites_row->child(i);
+ if (game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
+ favorites_row->removeRow(i);
+ return;
+ }
+ }
+}
+
QString GameList::FindGameByProgramID(u64 program_id, int role) {
return FindGameByProgramID(item_model->invisibleRootItem(), program_id, role);
}
diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h
index 9b9ccb05a..f7969850a 100644
--- a/src/citra_qt/game_list.h
+++ b/src/citra_qt/game_list.h
@@ -80,6 +80,10 @@ public:
void RefreshGameDirectory();
+ void ToggleFavorite(u64 program_id);
+ void AddFavorite(u64 program_id);
+ void RemoveFavorite(u64 program_id);
+
static const QStringList supported_file_extensions;
signals:
@@ -113,6 +117,7 @@ private:
u64 extdata_id, Service::FS::MediaType media_type);
void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
+ void AddFavoritesPopup(QMenu& context_menu);
void UpdateColumnVisibility();
QString FindGameByProgramID(QStandardItem* current_item, u64 program_id, int role);
diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h
index 338d8cee7..16a7b5a8f 100644
--- a/src/citra_qt/game_list_p.h
+++ b/src/citra_qt/game_list_p.h
@@ -34,7 +34,8 @@ enum class GameListItemType {
CustomDir = QStandardItem::UserType + 2,
InstalledDir = QStandardItem::UserType + 3,
SystemDir = QStandardItem::UserType + 4,
- AddDir = QStandardItem::UserType + 5
+ AddDir = QStandardItem::UserType + 5,
+ Favorites = QStandardItem::UserType + 6,
};
Q_DECLARE_METATYPE(GameListItemType);
@@ -430,6 +431,28 @@ public:
}
};
+class GameListFavorites : public GameListItem {
+public:
+ explicit GameListFavorites() {
+ setData(type(), TypeRole);
+
+ const int icon_size = IconSizes.at(UISettings::values.game_list_icon_size.GetValue());
+ setData(QIcon::fromTheme(QStringLiteral("star"))
+ .pixmap(icon_size)
+ .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+ Qt::DecorationRole);
+ setData(QObject::tr("Favorites"), Qt::DisplayRole);
+ }
+
+ int type() const override {
+ return static_cast(GameListItemType::Favorites);
+ }
+
+ bool operator<(const QStandardItem& other) const override {
+ return false;
+ }
+};
+
class GameList;
class QHBoxLayout;
class QTreeView;
@@ -444,6 +467,7 @@ public:
explicit GameListSearchField(GameList* parent = nullptr);
void setFilterResult(int visible, int total);
+ bool IsEmpty() const;
void clear();
void setFocus();
diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h
index 8e671f49f..e85e139a9 100644
--- a/src/citra_qt/uisettings.h
+++ b/src/citra_qt/uisettings.h
@@ -116,6 +116,8 @@ struct Values {
bool game_dir_deprecated_deepscan;
QVector game_dirs;
QStringList recent_files;
+ QVector favorited_ids;
+
QString language;
QString theme;