Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/resources/icons/star-fill.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions pcsx2-qt/GameList/GameListModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@ QVariant GameListModel::data(const QModelIndex& index, const int role) const
{
switch (index.column())
{
case Column_Title:
case Column_FileTitle:
if (ge->is_favorite)
return m_favorite_pixmap;
return QVariant();
case Column_Type:
return m_type_pixmaps[static_cast<u32>(ge->type)];

Expand Down Expand Up @@ -310,6 +315,13 @@ QVariant GameListModel::data(const QModelIndex& index, const int role) const
}
}

case NeedsFavoriteBadgeRole:
{
if (index.column() == Column_Cover)
return ge->is_favorite;
return QVariant();
}

case Qt::SizeHintRole:
{
switch (index.column())
Expand Down Expand Up @@ -376,6 +388,10 @@ bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& r
if (!left || !right)
return false;

// Favorites always sort to the top regardless of column
if (left->is_favorite != right->is_favorite)
return left->is_favorite;

switch (column)
{
case Column_Type:
Expand Down Expand Up @@ -504,6 +520,7 @@ void GameListModel::loadCommonImages()
m_compatibility_pixmaps[rating] = QIcon((QStringLiteral("%1/icons/star-%2.svg").arg(base_path).arg(rating - 1))).pixmap(QSize(88, 16), m_dpr);

m_placeholder_pixmap.load(QStringLiteral("%1/cover-placeholder.png").arg(base_path));
m_favorite_pixmap = QIcon(QStringLiteral("%1/icons/star-fill.svg").arg(base_path)).pixmap(QSize(16, 16), m_dpr);
}

void GameListModel::setColumnDisplayNames()
Expand Down
7 changes: 7 additions & 0 deletions pcsx2-qt/GameList/GameListModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class GameListModel final : public QAbstractTableModel
Column_Count
};

enum : int
{
NeedsFavoriteBadgeRole = Qt::UserRole
};

static std::optional<Column> getColumnIdForName(std::string_view name);
static const char* getColumnName(Column col);

Expand All @@ -62,6 +67,7 @@ class GameListModel final : public QAbstractTableModel

bool getShowCoverTitles() const { return m_show_titles_for_covers; }
void setShowCoverTitles(bool enabled) { m_show_titles_for_covers = enabled; }
const QPixmap& getFavoritePixmap() const { return m_favorite_pixmap; }

float getCoverScale() const { return m_cover_scale; }
void setCoverScale(float scale);
Expand Down Expand Up @@ -99,5 +105,6 @@ class GameListModel final : public QAbstractTableModel
qreal m_dpr;

std::array<QPixmap, static_cast<int>(GameList::CompatibilityRatingCount)> m_compatibility_pixmaps;
QPixmap m_favorite_pixmap;
mutable LRUCache<std::string, QPixmap> m_cover_pixmap_cache;
};
61 changes: 45 additions & 16 deletions pcsx2-qt/GameList/GameListWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,17 @@ static constexpr int DEFAULT_SORT_INDEX = static_cast<int>(DEFAULT_SORT_COLUMN);
static constexpr Qt::SortOrder DEFAULT_SORT_ORDER = Qt::AscendingOrder;

static constexpr std::array<int, GameListModel::Column_Count> DEFAULT_COLUMN_WIDTHS = {{
55, // type
85, // code
-1, // title
-1, // file title
75, // crc
95, // time played
90, // last played
80, // size
60, // region
120 // compatibility
55, // type
85, // code
-1, // title
-1, // file title
75, // crc
95, // time played
90, // last played
80, // size
60, // region
120, // compatibility
-1, // cover
}};
static_assert(static_cast<int>(DEFAULT_COLUMN_WIDTHS.size()) <= GameListModel::Column_Count,
"Game List: More default column widths than column types.");
Expand Down Expand Up @@ -117,6 +118,15 @@ class GameListSortModel final : public QSortFilterProxyModel

bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override
{
const auto lock = GameList::GetLock();
const GameList::Entry* left = GameList::GetEntryByIndex(source_left.row());
const GameList::Entry* right = GameList::GetEntryByIndex(source_right.row());

// Favorites always sort to the top regardless of column or sort direction.

if (left && right && left->is_favorite != right->is_favorite)
return sortOrder() == Qt::AscendingOrder ? left->is_favorite : right->is_favorite;

return m_model->lessThan(source_left, source_right, source_left.column());
}

Expand All @@ -133,6 +143,8 @@ namespace
class GameListIconStyleDelegate final : public QStyledItemDelegate
{
public:
static constexpr int STAR_MARGIN = 4;

GameListIconStyleDelegate(QWidget* parent)
: QStyledItemDelegate(parent)
{
Expand Down Expand Up @@ -163,7 +175,7 @@ namespace
// Determine starting location of icon (Qt uses top-left origin).
const int icon_width = static_cast<int>(static_cast<qreal>(icon.width()) / icon.devicePixelRatio());
const int icon_height = static_cast<int>(static_cast<qreal>(icon.height()) / icon.devicePixelRatio());
const QPoint icon_top_left = QPoint((rect.width() - icon_width) / 2, (rect.height() - icon_height) / 2);
const QPoint icon_top_left = rect.topLeft() + QPoint((rect.width() - icon_width) / 2, (rect.height() - icon_height) / 2);

// Change palette if the item is selected.
if (option.state & QStyle::State_Selected)
Expand All @@ -189,16 +201,32 @@ namespace
QPixmapCache::insert(key, highlighted_icon);
}

painter->drawPixmap(rect.topLeft() + icon_top_left, highlighted_icon);
painter->drawPixmap(icon_top_left, highlighted_icon);
}
else
{
painter->drawPixmap(rect.topLeft() + icon_top_left, icon);
painter->drawPixmap(icon_top_left, icon);
}

// Draw star overlay on bottom-right corner if game is favorited.
const bool is_favorite = (index.column() == GameListModel::Column_Cover) &&
index.data(GameListModel::NeedsFavoriteBadgeRole).toBool();
if (is_favorite)
{
const auto* sort_model = static_cast<const QSortFilterProxyModel*>(index.model());
const auto* game_model = static_cast<const GameListModel*>(sort_model->sourceModel());
const QPixmap& star = game_model->getFavoritePixmap();
const QPoint icon_bottom_right = icon_top_left + QPoint(icon_width, icon_height);
const QSizeF size = star.deviceIndependentSize();
const QPoint star_pos = icon_bottom_right - QPoint(size.width() + STAR_MARGIN, size.height() + STAR_MARGIN);
painter->drawPixmap(star_pos, star);
}

// Restore the old clip path.
painter->restore();
}

private:
};
} // namespace

Expand Down Expand Up @@ -272,9 +300,9 @@ void GameListWidget::initialize()
m_table_view->setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel);

// Custom painter to center-align DisplayRoles (icons)
m_table_view->setItemDelegateForColumn(0, new GameListIconStyleDelegate(this));
m_table_view->setItemDelegateForColumn(8, new GameListIconStyleDelegate(this));
m_table_view->setItemDelegateForColumn(9, new GameListIconStyleDelegate(this));
m_table_view->setItemDelegateForColumn(static_cast<int>(GameListModel::Column_Type), new GameListIconStyleDelegate(this));
m_table_view->setItemDelegateForColumn(static_cast<int>(GameListModel::Column_Region), new GameListIconStyleDelegate(this));
m_table_view->setItemDelegateForColumn(static_cast<int>(GameListModel::Column_Compatibility), new GameListIconStyleDelegate(this));

connect(m_table_view->selectionModel(), &QItemSelectionModel::currentChanged, this,
&GameListWidget::onSelectionModelCurrentChanged);
Expand Down Expand Up @@ -318,6 +346,7 @@ void GameListWidget::initialize()
m_list_view = new GameListGridListView(m_ui.stack);
m_list_view->setModel(m_sort_model);
m_list_view->setModelColumn(GameListModel::Column_Cover);
m_list_view->setItemDelegate(new GameListIconStyleDelegate(this));
m_list_view->setSelectionMode(QAbstractItemView::SingleSelection);
m_list_view->setViewMode(QListView::IconMode);
m_list_view->setResizeMode(QListView::Adjust);
Expand Down
6 changes: 6 additions & 0 deletions pcsx2-qt/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,12 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
#if !defined(__APPLE__)
connect(menu.addAction(tr("Create Game Shortcut...")), &QAction::triggered, [this]() { MainWindow::onCreateGameShortcutTriggered(); });
#endif
const bool is_favorite = entry->is_favorite;
action = menu.addAction(is_favorite ? tr("Remove from Favorites") : tr("Add to Favorites"));
connect(action, &QAction::triggered, [this, entry]() {
GameList::SetGameFavorite(entry->path, !entry->is_favorite);
m_game_list_widget->refresh(false, false);
});

connect(menu.addAction(tr("Exclude From List")), &QAction::triggered,
[this, entry]() { getSettingsWindow()->getGameListSettingsWidget()->addExcludedPath(entry->path); });
Expand Down
42 changes: 39 additions & 3 deletions pcsx2/GameList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ namespace GameList
static bool GetGameListEntryFromCache(const std::string& path, GameList::Entry* entry);
static void ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector<std::string>& excluded_paths,
const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini, ProgressCallback* progress);
static bool AddFileFromCache(const std::string& path, std::time_t timestamp, const PlayedTimeMap& played_time_map);
static bool AddFileFromCache(const std::string& path, std::time_t timestamp, const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini);
static bool ScanFile(std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock,
const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini);

Expand Down Expand Up @@ -714,7 +714,7 @@ void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
}

std::unique_lock lock(s_mutex);
if (GetEntryForPath(ffd.FileName.c_str()) || AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map) || only_cache)
if (GetEntryForPath(ffd.FileName.c_str()) || AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map, custom_attributes_ini) || only_cache)
{
continue;
}
Expand All @@ -729,7 +729,7 @@ void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
progress->PopState();
}

bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp, const PlayedTimeMap& played_time_map)
bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp, const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini)
{
Entry entry;
if (!GetGameListEntryFromCache(path, &entry) || entry.last_modified_time != timestamp)
Expand All @@ -746,6 +746,7 @@ bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp,
entry.total_played_time = iter->second.total_played_time;
}

entry.is_favorite = custom_attributes_ini.GetBoolValue(entry.path.c_str(), "Favorited", false);
s_entries.push_back(std::move(entry));
return true;
}
Expand Down Expand Up @@ -784,6 +785,8 @@ bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_loc
entry.total_played_time = iter->second.total_played_time;
}

entry.is_favorite = custom_attributes_ini.GetBoolValue(entry.path.c_str(), "Favorited", false);

auto custom_title = custom_attributes_ini.GetOptionalStringValue(entry.path.c_str(), "Title");
if (custom_title)
{
Expand Down Expand Up @@ -1459,6 +1462,39 @@ std::string GameList::GetCustomPropertiesFile()
return Path::Combine(EmuFolders::Settings, "custom_properties.ini");
}

bool GameList::IsGameFavorited(const std::string& path)
{
std::unique_lock lock(s_mutex);
const GameList::Entry* entry = GetEntryForPath(path.c_str());
if (entry)
return entry->is_favorite;

return false;
}

void GameList::SetGameFavorite(const std::string& path, bool favorite)
{
INISettingsInterface custom_attributes(GetCustomPropertiesFile());
custom_attributes.Load();

if (favorite)
{
custom_attributes.SetBoolValue(path.c_str(), "Favorited", true);
}
else
{
custom_attributes.DeleteValue(path.c_str(), "Favorited");
}

if (custom_attributes.Save())
{
std::unique_lock lock(s_mutex);
GameList::Entry* entry = const_cast<GameList::Entry*>(GetEntryForPath(path.c_str()));
if (entry)
entry->is_favorite = favorite;
}
}

void GameList::CheckCustomAttributesForPath(const std::string& path, bool& has_custom_title, bool& has_custom_region)
{
INISettingsInterface names(GetCustomPropertiesFile());
Expand Down
3 changes: 3 additions & 0 deletions pcsx2/GameList.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ namespace GameList
u32 crc = 0;

CompatibilityRating compatibility_rating = CompatibilityRating::Unknown;
bool is_favorite = false;

__fi bool IsDisc() const { return (type == EntryType::PS1Disc || type == EntryType::PS2Disc); }
};
Expand Down Expand Up @@ -157,6 +158,8 @@ namespace GameList
std::function<void(const Entry*, std::string)> save_callback = {});

// Custom properties support
bool IsGameFavorited(const std::string& path);
void SetGameFavorite(const std::string& path, bool favorite);
void CheckCustomAttributesForPath(const std::string& path, bool& has_custom_title, bool& has_custom_region);
void SaveCustomTitleForPath(const std::string& path, const std::string& custom_title);
void SaveCustomRegionForPath(const std::string& path, int custom_region);
Expand Down
Loading