Improve Game Collections (#19349)

Follow up of #19292 to improve Game Collections in context menu:
- Add and remove the game to/from multiple collections (e.g. "3rd person" and "Fantasy")
- Create a collection and add the game to it
- Remove the game from current collection if it is not the "All games"
This commit is contained in:
Antonino Di Guardo
2026-08-29 18:56:56 +00:00
committed by GitHub
parent 01c59b99a7
commit 010bf1753e
5 changed files with 101 additions and 113 deletions
+70 -61
View File
@@ -608,6 +608,13 @@ void game_list_actions::ShowDiskUsageDialog()
});
}
// How a game collection is listed in either menu: its name, and how many of its games the game list is
// showing. One pass: a collection may be named "%1", and a chained arg() would substitute into it.
static QString collection_entry_text(const QString& name, qsizetype count)
{
return QString("%0 (%1)").arg(gui::utils::escape_mnemonics(name), QString::number(count));
}
void game_list_actions::UpdateGameCollectionMenu(QMenu* menu, QActionGroup* act_group, QMenu* rename_menu, QMenu* remove_menu)
{
const QStringList collections = m_gui_settings->GetGameCollections();
@@ -664,20 +671,25 @@ void game_list_actions::UpdateGameCollectionMenu(QMenu* menu, QActionGroup* act_
for (const QString& name : collections)
{
// One pass: a collection may be named "%1", and a chained arg() would substitute into it.
add_entry(QString("%0 (%1)").arg(gui::utils::escape_mnemonics(name),
QString::number(counts.value(name))), name);
add_entry(collection_entry_text(name, counts.value(name)), name);
}
}
void game_list_actions::CreateGameCollection()
void game_list_actions::CreateGameCollection(const QSet<QString>& serials)
{
const QString name = AskForCollectionName(tr("Create Game Collection"), {},
[this](const QString& to) { return m_gui_settings->AddGameCollection(to); });
if (!name.isEmpty())
if (name.isEmpty())
{
game_list_log.notice("Created game collection '%s'", name);
return;
}
game_list_log.notice("Created game collection '%s'", name);
if (!serials.isEmpty())
{
ChangeCollectionMembership(serials, name, true);
}
}
@@ -732,7 +744,7 @@ void game_list_actions::SelectGameCollection(const QString& name)
m_game_list_frame->SetGameCollection(name);
}
void game_list_actions::AddMoveToCollectionMenu(QMenu* parent, const std::vector<game_info>& games)
void game_list_actions::AddCollectionMenu(QMenu* parent, const std::vector<game_info>& games)
{
QSet<QString> serials;
@@ -750,51 +762,47 @@ void game_list_actions::AddMoveToCollectionMenu(QMenu* parent, const std::vector
}
const QStringList collections = m_gui_settings->GetGameCollections();
QMenu* collection_menu = parent->addMenu(tr("&Move To Collection"));
collection_menu->setEnabled(!collections.isEmpty());
if (collections.isEmpty())
{
return;
}
// A single game shows the collection it currently sits in. A multi selection is a bulk move to whatever
// entry is picked, no matter which collections the games come from, so it shows no state at all.
const bool is_single_game = serials.size() == 1;
const QString current = is_single_game ? m_gui_settings->GetCollectionOfGame(*serials.cbegin()) : QString();
QActionGroup* collection_act_group = is_single_game ? new QActionGroup(collection_menu) : nullptr;
const auto add_entry = [&](const QString& text, const QString& name)
{
QAction* act = collection_menu->addAction(text);
if (collection_act_group)
{
act->setCheckable(true);
act->setChecked(name == current);
collection_act_group->addAction(act);
}
// Moving one game is trivially undone. Moving a block is not: the games can come from several
// collections at once and where each of them was is not recorded anywhere, so that one asks first.
connect(act, &QAction::triggered, this, [this, serials, name, is_single_game]()
{
MoveGamesToCollection(serials, name, !is_single_game);
});
};
const QHash<QString, qsizetype> counts = m_game_list_frame->CountGamesPerCollection(collections);
add_entry(gui_settings::GetAllGamesCollectionLabel(), {});
QMenu* collection_menu = parent->addMenu(tr("&Add to Collection"));
// Always there: with no collection yet this is the only thing the submenu can offer
connect(collection_menu->addAction(tr("&Create and Add")), &QAction::triggered, this, [this, serials]()
{
CreateGameCollection(serials);
});
if (!collections.isEmpty())
{
collection_menu->addSeparator();
}
for (const QString& collection : collections)
{
// One pass: a collection may be named "%1", and a chained arg() would substitute into it.
const QString text = QString("%0 (%1)").arg(gui::utils::escape_mnemonics(collection),
QString::number(counts.value(collection)));
add_entry(text, collection);
// A multi selection is ticked only once every game in it belongs to the collection, so that the
// entry finishes adding the ones that are missing before it starts taking any out
const bool is_member = m_gui_settings->GetGamesInCollection(collection).contains(serials);
QAction* act = collection_menu->addAction(collection_entry_text(collection, counts.value(collection)));
act->setCheckable(true);
act->setChecked(is_member);
connect(act, &QAction::triggered, this, [this, serials, collection, is_member]()
{
ChangeCollectionMembership(serials, collection, !is_member);
});
}
const QString current = m_gui_settings->GetCurrentGameCollection();
// Acts on the collection the game list is filtered by, which is the one the user is looking at
if (!current.isEmpty() && m_gui_settings->GetGamesInCollection(current).intersects(serials))
{
connect(parent->addAction(tr("&Remove from Collection '%0'").arg(gui::utils::escape_mnemonics(current))),
&QAction::triggered, this, [this, serials, current]()
{
ChangeCollectionMembership(serials, current, false);
});
}
}
@@ -851,39 +859,40 @@ QString game_list_actions::AskForCollectionName(const QString& title, const QStr
return {};
}
void game_list_actions::MoveGamesToCollection(const QSet<QString>& serials, const QString& name, bool is_interactive)
void game_list_actions::ChangeCollectionMembership(const QSet<QString>& serials, const QString& name, bool add)
{
const bool moves_anything = !m_gui_settings->GetGamesInCollection(name).contains(serials);
if (is_interactive && moves_anything)
// Adding is undone by the very entry that did it, but a removal takes memberships that were given one
// game at a time and cannot be handed back the same way, so a block of them asks first
if (!add && serials.size() > 1)
{
const QString question = name.isEmpty()
? tr("Remove %Ln game(s) from their game collection?", "", static_cast<int>(serials.size()))
: tr("Move %Ln game(s) to the '%0' game collection?", "", static_cast<int>(serials.size())).arg(name);
const QString question = tr("Remove %Ln game(s) from the '%0' game collection?", "",
static_cast<int>(serials.size())).arg(name);
if (gui::utils::plain_message(m_game_list_frame, QMessageBox::Question, tr("Confirm Move"), question,
if (gui::utils::plain_message(m_game_list_frame, QMessageBox::Question, tr("Confirm Removal"), question,
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
{
return;
}
}
// The user picked the collection the games already sit in
if (!m_gui_settings->MoveGamesToCollection(serials, name))
const bool changed = m_gui_settings->SetGameCollectionMembership(serials, name, add);
// The menu was built before the collection changed under it
if (!changed)
{
return;
}
if (name.isEmpty())
if (add)
{
game_list_log.notice("Removed %d game(s) from their game collection", serials.size());
game_list_log.notice("Added %d game(s) to game collection '%s'", serials.size(), name);
}
else
{
game_list_log.notice("Moved %d game(s) to game collection '%s'", serials.size(), name);
game_list_log.notice("Removed %d game(s) from game collection '%s'", serials.size(), name);
}
// The moved games may have entered or left the collection the game list is filtered by
// The games may have entered or left the collection the game list is filtered by
m_game_list_frame->ReloadGameCollection();
}
+9 -6
View File
@@ -66,8 +66,9 @@ public:
// submenus belong to the Manage menu, and are null for the View one, which carries neither.
void UpdateGameCollectionMenu(QMenu* menu, QActionGroup* act_group, QMenu* rename_menu, QMenu* remove_menu);
// Asks for a name until it is accepted or the dialog is dismissed
void CreateGameCollection();
// Asks for a name until it is accepted or the dialog is dismissed, then adds the given games to what
// was created
void CreateGameCollection(const QSet<QString>& serials = {});
// Asks for a new name for a game collection until it is accepted or the dialog is dismissed
void RenameGameCollection(const QString& name);
@@ -78,8 +79,10 @@ public:
// Makes a game collection the one the game list is filtered by. An empty name shows every game.
void SelectGameCollection(const QString& name);
// Appends the "Move To Collection" submenu, which moves the given games to a user defined game collection
void AddMoveToCollectionMenu(QMenu* parent, const std::vector<game_info>& games);
// Appends the "Add to Collection" submenu, which puts the given games in a user defined game collection
// or takes them out of it, and the entry that removes them from the collection the game list is
// filtered by
void AddCollectionMenu(QMenu* parent, const std::vector<game_info>& games);
// NOTES:
// - SetContentList() MUST always be called to set the content's info to be removed by:
@@ -140,8 +143,8 @@ private:
QString AskForCollectionName(const QString& title, const QString& initial,
const std::function<bool(const QString&)>& accept);
// Performs what an entry of the "Move To Collection" submenu does. Confirms first when interactive.
void MoveGamesToCollection(const QSet<QString>& serials, const QString& name, bool is_interactive);
// Puts the given games in a game collection or takes them out of it, then refreshes the game list
void ChangeCollectionMembership(const QSet<QString>& serials, const QString& name, bool add);
void BatchActionBySerials(progress_dialog* pdlg, const std::set<std::string>& serials,
QString progressLabel, std::function<bool(const std::string&)> action,
+4 -4
View File
@@ -326,8 +326,8 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info&
manage_game_menu->addSeparator();
// Move the game to a user defined game collection
m_game_list_actions->AddMoveToCollectionMenu(manage_game_menu, {gameinfo});
// Add the game to user defined game collections
m_game_list_actions->AddCollectionMenu(manage_game_menu, {gameinfo});
// Hide/rename game in game list
QAction* hide_hidden_serial = manage_game_menu->addAction(tr("&Hide Game In Game List"));
@@ -965,8 +965,8 @@ void game_list_context_menu::show_multi_selection_context_menu(const std::vector
manage_game_menu->addSeparator();
// Move the games to a user defined game collection
m_game_list_actions->AddMoveToCollectionMenu(manage_game_menu, games);
// Add the games to user defined game collections
m_game_list_actions->AddCollectionMenu(manage_game_menu, games);
// Hide game in game list
QAction* hide_hidden_serial = manage_game_menu->addAction(tr("&Hide Game In Game List"));
+15 -36
View File
@@ -306,19 +306,6 @@ QSet<QString> gui_settings::GetGamesInCollection(const QString& name) const
return gui::utils::list_to_set(GetValue(gui::game_collection, gc_games_prefix + name, QStringList()).toStringList());
}
QString gui_settings::GetCollectionOfGame(const QString& serial) const
{
for (const QString& collection : GetGameCollections())
{
if (GetGamesInCollection(collection).contains(serial))
{
return collection;
}
}
return {};
}
bool gui_settings::CleanupCollections(const QSet<QString>& serials) const
{
const QStringList& collections = GetGameCollections();
@@ -352,42 +339,34 @@ bool gui_settings::CleanupCollections(const QSet<QString>& serials) const
return changed;
}
bool gui_settings::MoveGamesToCollection(const QSet<QString>& serials, const QString& name) const
bool gui_settings::SetGameCollectionMembership(const QSet<QString>& serials, const QString& name, bool add) const
{
const QStringList collections = GetGameCollections();
if (serials.isEmpty() || (!name.isEmpty() && !collections.contains(name)))
if (serials.isEmpty() || !GetGameCollections().contains(name))
{
return false;
}
bool changed = false;
QSet<QString> games = GetGamesInCollection(name);
const qsizetype old_size = games.size();
for (const QString& collection : collections)
if (add)
{
games.unite(serials);
}
else
{
const QSet<QString> old_games = GetGamesInCollection(collection);
QSet<QString> games = old_games;
games.subtract(serials);
if (collection == name)
{
games.unite(serials);
}
if (games != old_games)
{
SetGamesInCollection(collection, games);
changed = true;
}
}
if (changed)
// unite only adds and subtract only removes, so a size that did not move is a set that did not either
if (games.size() == old_size)
{
sync();
return false;
}
return changed;
SetGamesInCollection(name, games);
sync();
return true;
}
QString gui_settings::GetAllGamesCollectionLabel()
+3 -6
View File
@@ -378,16 +378,13 @@ public:
void SetCurrentGameCollection(const QString& name, bool sync = true) const;
QSet<QString> GetGamesInCollection(const QString& name) const;
/** Returns the game collection the given game belongs to, empty if it belongs to none */
QString GetCollectionOfGame(const QString& serial) const;
/** Cleans up all game collections according to the provided serials.
Returns whether anything actually changed. */
bool CleanupCollections(const QSet<QString>& serials) const;
/** Moves the given games to a game collection, removing them from any other one. An empty name only
removes them. Returns whether anything actually changed. */
bool MoveGamesToCollection(const QSet<QString>& serials, const QString& name) const;
/** Puts the given games in a game collection or takes them out of it, leaving every collection they
already belong to alone. Returns whether anything actually changed. */
bool SetGameCollectionMembership(const QSet<QString>& serials, const QString& name, bool add) const;
/** Label of the default game collection entry. Reserved, so it can never name a real collection. */
static QString GetAllGamesCollectionLabel();