mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-26 07:35:41 +00:00
GameList: Merge multi-disc games
This commit is contained in:
parent
9bdf23cba7
commit
1adaea9005
|
@ -464,6 +464,7 @@ static void DrawGameList(const ImVec2& heading_size);
|
|||
static void DrawGameGrid(const ImVec2& heading_size);
|
||||
static void HandleGameListActivate(const GameList::Entry* entry);
|
||||
static void HandleGameListOptions(const GameList::Entry* entry);
|
||||
static void HandleSelectDiscForDiscSet(std::string_view disc_set_name);
|
||||
static void DrawGameListSettingsWindow();
|
||||
static void SwitchToGameList();
|
||||
static void PopulateGameListEntryList();
|
||||
|
@ -5919,7 +5920,7 @@ bool FullscreenUI::OpenLoadStateSelectorForGameResume(const GameList::Entry* ent
|
|||
|
||||
void FullscreenUI::DrawResumeStateSelector()
|
||||
{
|
||||
ImGui::SetNextWindowSize(LayoutScale(800.0f, 600.0f));
|
||||
ImGui::SetNextWindowSize(LayoutScale(800.0f, 602.0f));
|
||||
ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||
ImGui::OpenPopup(FSUI_CSTR("Load Resume State"));
|
||||
|
||||
|
@ -6048,11 +6049,27 @@ void FullscreenUI::PopulateGameListEntryList()
|
|||
{
|
||||
const s32 sort = Host::GetBaseIntSettingValue("Main", "FullscreenUIGameSort", 0);
|
||||
const bool reverse = Host::GetBaseBoolSettingValue("Main", "FullscreenUIGameSortReverse", false);
|
||||
const bool merge_disc_sets = Host::GetBaseBoolSettingValue("Main", "FullscreenUIMergeDiscSets", true);
|
||||
|
||||
const u32 count = GameList::GetEntryCount();
|
||||
s_game_list_sorted_entries.resize(count);
|
||||
s_game_list_sorted_entries.clear();
|
||||
s_game_list_sorted_entries.reserve(count);
|
||||
for (u32 i = 0; i < count; i++)
|
||||
s_game_list_sorted_entries[i] = GameList::GetEntryByIndex(i);
|
||||
{
|
||||
const GameList::Entry* entry = GameList::GetEntryByIndex(i);
|
||||
if (merge_disc_sets)
|
||||
{
|
||||
if (entry->disc_set_member)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (entry->IsDiscSet())
|
||||
continue;
|
||||
}
|
||||
|
||||
s_game_list_sorted_entries.push_back(entry);
|
||||
}
|
||||
|
||||
std::sort(s_game_list_sorted_entries.begin(), s_game_list_sorted_entries.end(),
|
||||
[sort, reverse](const GameList::Entry* lhs, const GameList::Entry* rhs) {
|
||||
|
@ -6539,6 +6556,12 @@ void FullscreenUI::DrawGameGrid(const ImVec2& heading_size)
|
|||
|
||||
void FullscreenUI::HandleGameListActivate(const GameList::Entry* entry)
|
||||
{
|
||||
if (entry->IsDiscSet())
|
||||
{
|
||||
HandleSelectDiscForDiscSet(entry->path);
|
||||
return;
|
||||
}
|
||||
|
||||
// launch game
|
||||
if (!OpenLoadStateSelectorForGameResume(entry))
|
||||
DoStartPath(entry->path);
|
||||
|
@ -6546,53 +6569,121 @@ void FullscreenUI::HandleGameListActivate(const GameList::Entry* entry)
|
|||
|
||||
void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry)
|
||||
{
|
||||
ImGuiFullscreen::ChoiceDialogOptions options = {
|
||||
{FSUI_ICONSTR(ICON_FA_WRENCH, "Game Properties"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Open Containing Directory"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_PLAY, "Resume Game"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_UNDO, "Load State"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Default Boot"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Fast Boot"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_MAGIC, "Slow Boot"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_FOLDER_MINUS, "Reset Play Time"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false},
|
||||
};
|
||||
if (!entry->IsDiscSet())
|
||||
{
|
||||
ImGuiFullscreen::ChoiceDialogOptions options = {
|
||||
{FSUI_ICONSTR(ICON_FA_WRENCH, "Game Properties"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Open Containing Directory"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_PLAY, "Resume Game"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_UNDO, "Load State"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Default Boot"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Fast Boot"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_MAGIC, "Slow Boot"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_FOLDER_MINUS, "Reset Play Time"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false},
|
||||
};
|
||||
|
||||
OpenChoiceDialog(
|
||||
entry->title.c_str(), false, std::move(options),
|
||||
[entry_path = entry->path, entry_serial = entry->serial](s32 index, const std::string& title, bool checked) {
|
||||
switch (index)
|
||||
{
|
||||
case 0: // Open Game Properties
|
||||
SwitchToGameSettingsForPath(entry_path);
|
||||
break;
|
||||
case 1: // Open Containing Directory
|
||||
ExitFullscreenAndOpenURL(Path::CreateFileURL(Path::GetDirectory(entry_path)));
|
||||
break;
|
||||
case 2: // Resume Game
|
||||
DoStartPath(entry_path, System::GetGameSaveStateFileName(entry_serial, -1));
|
||||
break;
|
||||
case 3: // Load State
|
||||
OpenLoadStateSelectorForGame(entry_path);
|
||||
break;
|
||||
case 4: // Default Boot
|
||||
DoStartPath(entry_path);
|
||||
break;
|
||||
case 5: // Fast Boot
|
||||
DoStartPath(entry_path, {}, true);
|
||||
break;
|
||||
case 6: // Slow Boot
|
||||
DoStartPath(entry_path, {}, false);
|
||||
break;
|
||||
case 7: // Reset Play Time
|
||||
GameList::ClearPlayedTimeForSerial(entry_serial);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
OpenChoiceDialog(
|
||||
entry->title.c_str(), false, std::move(options),
|
||||
[entry_path = entry->path, entry_serial = entry->serial](s32 index, const std::string& title, bool checked) {
|
||||
switch (index)
|
||||
{
|
||||
case 0: // Open Game Properties
|
||||
SwitchToGameSettingsForPath(entry_path);
|
||||
break;
|
||||
case 1: // Open Containing Directory
|
||||
ExitFullscreenAndOpenURL(Path::CreateFileURL(Path::GetDirectory(entry_path)));
|
||||
break;
|
||||
case 2: // Resume Game
|
||||
DoStartPath(entry_path, System::GetGameSaveStateFileName(entry_serial, -1));
|
||||
break;
|
||||
case 3: // Load State
|
||||
OpenLoadStateSelectorForGame(entry_path);
|
||||
break;
|
||||
case 4: // Default Boot
|
||||
DoStartPath(entry_path);
|
||||
break;
|
||||
case 5: // Fast Boot
|
||||
DoStartPath(entry_path, {}, true);
|
||||
break;
|
||||
case 6: // Slow Boot
|
||||
DoStartPath(entry_path, {}, false);
|
||||
break;
|
||||
case 7: // Reset Play Time
|
||||
GameList::ClearPlayedTimeForSerial(entry_serial);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
CloseChoiceDialog();
|
||||
});
|
||||
CloseChoiceDialog();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// shouldn't fail
|
||||
const GameList::Entry* first_disc_entry = GameList::GetFirstDiscSetMember(entry->path);
|
||||
if (!first_disc_entry)
|
||||
return;
|
||||
|
||||
ImGuiFullscreen::ChoiceDialogOptions options = {
|
||||
{FSUI_ICONSTR(ICON_FA_WRENCH, "Game Properties"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc"), false},
|
||||
{FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false},
|
||||
};
|
||||
|
||||
OpenChoiceDialog(entry->title.c_str(), false, std::move(options),
|
||||
[entry_path = first_disc_entry->path,
|
||||
disc_set_name = entry->path](s32 index, const std::string& title, bool checked) {
|
||||
switch (index)
|
||||
{
|
||||
case 0: // Open Game Properties
|
||||
SwitchToGameSettingsForPath(entry_path);
|
||||
break;
|
||||
case 1: // Select Disc
|
||||
HandleSelectDiscForDiscSet(disc_set_name);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
CloseChoiceDialog();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void FullscreenUI::HandleSelectDiscForDiscSet(std::string_view disc_set_name)
|
||||
{
|
||||
auto lock = GameList::GetLock();
|
||||
const std::vector<const GameList::Entry*> entries = GameList::GetDiscSetMembers(disc_set_name, true);
|
||||
if (entries.empty())
|
||||
return;
|
||||
|
||||
ImGuiFullscreen::ChoiceDialogOptions options;
|
||||
std::vector<std::string> paths;
|
||||
paths.reserve(entries.size());
|
||||
|
||||
for (const GameList::Entry* entry : entries)
|
||||
{
|
||||
std::string title = fmt::format(fmt::runtime(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Disc {} | {}")),
|
||||
entry->disc_set_index + 1, Path::GetFileName(entry->path));
|
||||
options.emplace_back(std::move(title), false);
|
||||
paths.push_back(entry->path);
|
||||
}
|
||||
options.emplace_back(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close Menu"), false);
|
||||
|
||||
OpenChoiceDialog(SmallString::from_format("Select Disc for {}", disc_set_name), false, std::move(options),
|
||||
[paths = std::move(paths)](s32 index, const std::string& title, bool checked) {
|
||||
if (static_cast<u32>(index) < paths.size())
|
||||
{
|
||||
auto lock = GameList::GetLock();
|
||||
const GameList::Entry* entry = GameList::GetEntryForPath(paths[index]);
|
||||
if (entry)
|
||||
HandleGameListActivate(entry);
|
||||
}
|
||||
|
||||
CloseChoiceDialog();
|
||||
});
|
||||
}
|
||||
|
||||
void FullscreenUI::DrawGameListSettingsWindow()
|
||||
|
@ -6742,6 +6833,9 @@ void FullscreenUI::DrawGameListSettingsWindow()
|
|||
bsi, FSUI_ICONSTR(ICON_FA_SORT_ALPHA_DOWN, "Sort Reversed"),
|
||||
FSUI_CSTR("Reverses the game list sort order from the default (usually ascending to descending)."), "Main",
|
||||
"FullscreenUIGameSortReverse", false);
|
||||
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_LIST, "Merge Multi-Disc Games"),
|
||||
FSUI_CSTR("Merges multi-disc games into one item in the game list."), "Main",
|
||||
"FullscreenUIMergeDiscSets", true);
|
||||
}
|
||||
|
||||
MenuHeading(FSUI_CSTR("Cover Settings"));
|
||||
|
|
|
@ -82,6 +82,7 @@ static bool OpenCacheForWriting();
|
|||
static bool WriteEntryToCache(const Entry* entry);
|
||||
static void CloseCacheFileStream();
|
||||
static void DeleteCacheFile();
|
||||
static void CreateDiscSetEntries(const PlayedTimeMap& played_time_map);
|
||||
|
||||
static std::string GetPlayedTimeFile();
|
||||
static bool ParsePlayedTimeLine(char* line, std::string& serial, PlayedTimeEntry& entry);
|
||||
|
@ -100,15 +101,16 @@ static bool s_game_list_loaded = false;
|
|||
|
||||
const char* GameList::GetEntryTypeName(EntryType type)
|
||||
{
|
||||
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {{"Disc", "PSExe", "Playlist", "PSF"}};
|
||||
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {
|
||||
{"Disc", "DiscSet", "PSExe", "Playlist", "PSF"}};
|
||||
return names[static_cast<int>(type)];
|
||||
}
|
||||
|
||||
const char* GameList::GetEntryTypeDisplayName(EntryType type)
|
||||
{
|
||||
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {
|
||||
{TRANSLATE_NOOP("GameList", "Disc"), TRANSLATE_NOOP("GameList", "PS-EXE"), TRANSLATE_NOOP("GameList", "Playlist"),
|
||||
TRANSLATE_NOOP("GameList", "PSF")}};
|
||||
{TRANSLATE_NOOP("GameList", "Disc"), TRANSLATE_NOOP("GameList", "Disc Set"), TRANSLATE_NOOP("GameList", "PS-EXE"),
|
||||
TRANSLATE_NOOP("GameList", "Playlist"), TRANSLATE_NOOP("GameList", "PSF")}};
|
||||
return Host::TranslateToCString("GameList", names[static_cast<int>(type)]);
|
||||
}
|
||||
|
||||
|
@ -515,8 +517,8 @@ void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
|
|||
}
|
||||
|
||||
std::unique_lock lock(s_mutex);
|
||||
if (GetEntryForPath(ffd.FileName) ||
|
||||
AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map) || only_cache)
|
||||
if (GetEntryForPath(ffd.FileName) || AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map) ||
|
||||
only_cache)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -611,7 +613,7 @@ const GameList::Entry* GameList::GetEntryBySerial(std::string_view serial)
|
|||
{
|
||||
for (const Entry& entry : s_entries)
|
||||
{
|
||||
if (entry.serial == serial)
|
||||
if (entry.serial == serial)
|
||||
return &entry;
|
||||
}
|
||||
|
||||
|
@ -629,19 +631,51 @@ const GameList::Entry* GameList::GetEntryBySerialAndHash(std::string_view serial
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<const GameList::Entry*> GameList::GetDiscSetMembers(std::string_view disc_set_name)
|
||||
std::vector<const GameList::Entry*> GameList::GetDiscSetMembers(std::string_view disc_set_name,
|
||||
bool sort_by_most_recent)
|
||||
{
|
||||
std::vector<const Entry*> ret;
|
||||
for (const Entry& entry : s_entries)
|
||||
{
|
||||
if (/*!entry.disc_set_member || */ disc_set_name != entry.disc_set_name)
|
||||
if (!entry.disc_set_member || disc_set_name != entry.disc_set_name)
|
||||
continue;
|
||||
|
||||
ret.push_back(&entry);
|
||||
}
|
||||
|
||||
if (sort_by_most_recent)
|
||||
{
|
||||
std::sort(ret.begin(), ret.end(), [](const Entry* lhs, const Entry* rhs) {
|
||||
if (lhs->last_played_time == rhs->last_played_time)
|
||||
return (lhs->disc_set_index < rhs->disc_set_index);
|
||||
else
|
||||
return (lhs->last_played_time > rhs->last_played_time);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
std::sort(ret.begin(), ret.end(),
|
||||
[](const Entry* lhs, const Entry* rhs) { return (lhs->disc_set_index < rhs->disc_set_index); });
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
const GameList::Entry* GameList::GetFirstDiscSetMember(std::string_view disc_set_name)
|
||||
{
|
||||
for (const Entry& entry : s_entries)
|
||||
{
|
||||
if (!entry.disc_set_member || disc_set_name != entry.disc_set_name)
|
||||
continue;
|
||||
|
||||
// Disc set should not have been created without the first disc being present.
|
||||
if (entry.disc_set_index == 0)
|
||||
return &entry;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
u32 GameList::GetEntryCount()
|
||||
{
|
||||
return static_cast<u32>(s_entries.size());
|
||||
|
@ -703,6 +737,112 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
|
|||
// don't need unused cache entries
|
||||
CloseCacheFileStream();
|
||||
s_cache_map.clear();
|
||||
|
||||
// merge multi-disc games
|
||||
CreateDiscSetEntries(played_time);
|
||||
}
|
||||
|
||||
void GameList::CreateDiscSetEntries(const PlayedTimeMap& played_time_map)
|
||||
{
|
||||
std::unique_lock lock(s_mutex);
|
||||
|
||||
for (size_t i = 0; i < s_entries.size(); i++)
|
||||
{
|
||||
const Entry& entry = s_entries[i];
|
||||
|
||||
// only first discs can create sets
|
||||
if (entry.type != EntryType::Disc || entry.disc_set_member || entry.disc_set_index != 0)
|
||||
continue;
|
||||
|
||||
// already have a disc set by this name?
|
||||
const std::string& disc_set_name = entry.disc_set_name;
|
||||
if (GetEntryForPath(disc_set_name.c_str()))
|
||||
continue;
|
||||
|
||||
const GameDatabase::Entry* dbentry = GameDatabase::GetEntryForSerial(entry.serial);
|
||||
if (!dbentry)
|
||||
continue;
|
||||
|
||||
// need at least two discs for a set
|
||||
bool found_another_disc = false;
|
||||
for (const Entry& other_entry : s_entries)
|
||||
{
|
||||
if (other_entry.type != EntryType::Disc || other_entry.disc_set_member ||
|
||||
other_entry.disc_set_name != disc_set_name || other_entry.disc_set_index == entry.disc_set_index)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
found_another_disc = true;
|
||||
break;
|
||||
}
|
||||
if (!found_another_disc)
|
||||
{
|
||||
Log_DevFmt("Not creating disc set {}, only one disc found", disc_set_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
Entry set_entry;
|
||||
set_entry.type = EntryType::DiscSet;
|
||||
set_entry.region = entry.region;
|
||||
set_entry.path = disc_set_name;
|
||||
set_entry.serial = entry.serial;
|
||||
set_entry.title = entry.disc_set_name;
|
||||
set_entry.genre = entry.developer;
|
||||
set_entry.publisher = entry.publisher;
|
||||
set_entry.developer = entry.developer;
|
||||
set_entry.hash = entry.hash;
|
||||
set_entry.file_size = 0;
|
||||
set_entry.uncompressed_size = 0;
|
||||
set_entry.last_modified_time = entry.last_modified_time;
|
||||
set_entry.last_played_time = 0;
|
||||
set_entry.total_played_time = 0;
|
||||
set_entry.release_date = entry.release_date;
|
||||
set_entry.supported_controllers = entry.supported_controllers;
|
||||
set_entry.min_players = entry.min_players;
|
||||
set_entry.max_players = entry.max_players;
|
||||
set_entry.min_blocks = entry.min_blocks;
|
||||
set_entry.max_blocks = entry.max_blocks;
|
||||
set_entry.compatibility = entry.compatibility;
|
||||
|
||||
// figure out play time for all discs, and sum it
|
||||
// we do this via lookups, rather than the other entries, because of duplicates
|
||||
for (const std::string& set_serial : dbentry->disc_set_serials)
|
||||
{
|
||||
const auto it = played_time_map.find(set_serial);
|
||||
if (it == played_time_map.end())
|
||||
continue;
|
||||
|
||||
set_entry.last_played_time =
|
||||
(set_entry.last_played_time == 0) ?
|
||||
it->second.last_played_time :
|
||||
((it->second.last_played_time != 0) ? std::min(set_entry.last_played_time, it->second.last_played_time) :
|
||||
set_entry.last_played_time);
|
||||
set_entry.total_played_time += it->second.total_played_time;
|
||||
}
|
||||
|
||||
// mark all discs for this set as part of it, so we don't try to add them again, and for filtering
|
||||
u32 num_parts = 0;
|
||||
for (Entry& other_entry : s_entries)
|
||||
{
|
||||
if (other_entry.type != EntryType::Disc || other_entry.disc_set_member ||
|
||||
other_entry.disc_set_name != disc_set_name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Log_InfoFmt("Adding {} to disc set {}", other_entry.path, disc_set_name);
|
||||
other_entry.disc_set_member = true;
|
||||
set_entry.last_modified_time = std::min(set_entry.last_modified_time, other_entry.last_modified_time);
|
||||
set_entry.file_size += other_entry.file_size;
|
||||
set_entry.uncompressed_size += other_entry.uncompressed_size;
|
||||
num_parts++;
|
||||
}
|
||||
|
||||
Log_InfoFmt("Created disc set {} from {} entries", disc_set_name, num_parts);
|
||||
|
||||
// entry is done :)
|
||||
s_entries.push_back(std::move(set_entry));
|
||||
}
|
||||
}
|
||||
|
||||
std::string GameList::GetCoverImagePathForEntry(const Entry* entry)
|
||||
|
@ -959,9 +1099,23 @@ void GameList::AddPlayedTimeForSerial(const std::string& serial, std::time_t las
|
|||
Log_VerbosePrintf("Add %u seconds play time to %s -> now %u", static_cast<unsigned>(add_time), serial.c_str(),
|
||||
static_cast<unsigned>(pt.total_played_time));
|
||||
|
||||
const GameDatabase::Entry* dbentry = GameDatabase::GetEntryForSerial(serial);
|
||||
|
||||
std::unique_lock<std::recursive_mutex> lock(s_mutex);
|
||||
for (GameList::Entry& entry : s_entries)
|
||||
{
|
||||
// add it to the disc set, if any
|
||||
if (entry.type == EntryType::DiscSet)
|
||||
{
|
||||
if (dbentry && dbentry->disc_set_name == entry.path)
|
||||
{
|
||||
entry.last_played_time = pt.last_played_time;
|
||||
entry.total_played_time = pt.total_played_time;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.serial != serial)
|
||||
continue;
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ namespace GameList {
|
|||
enum class EntryType
|
||||
{
|
||||
Disc,
|
||||
DiscSet,
|
||||
PSExe,
|
||||
Playlist,
|
||||
PSF,
|
||||
|
@ -57,12 +58,14 @@ struct Entry
|
|||
u8 min_blocks = 0;
|
||||
u8 max_blocks = 0;
|
||||
s8 disc_set_index = -1;
|
||||
bool disc_set_member = false;
|
||||
|
||||
GameDatabase::CompatibilityRating compatibility = GameDatabase::CompatibilityRating::Unknown;
|
||||
|
||||
size_t GetReleaseDateString(char* buffer, size_t buffer_size) const;
|
||||
|
||||
ALWAYS_INLINE bool IsDisc() const { return (type == EntryType::Disc); }
|
||||
ALWAYS_INLINE bool IsDiscSet() const { return (type == EntryType::DiscSet); }
|
||||
};
|
||||
|
||||
const char* GetEntryTypeName(EntryType type);
|
||||
|
@ -80,7 +83,8 @@ const Entry* GetEntryByIndex(u32 index);
|
|||
const Entry* GetEntryForPath(std::string_view path);
|
||||
const Entry* GetEntryBySerial(std::string_view serial);
|
||||
const Entry* GetEntryBySerialAndHash(std::string_view serial, u64 hash);
|
||||
std::vector<const Entry*> GetDiscSetMembers(std::string_view disc_set_name);
|
||||
std::vector<const Entry*> GetDiscSetMembers(std::string_view disc_set_name, bool sort_by_most_recent = false);
|
||||
const Entry* GetFirstDiscSetMember(std::string_view disc_set_name);
|
||||
u32 GetEntryCount();
|
||||
|
||||
bool IsGameListLoaded();
|
||||
|
|
|
@ -140,6 +140,9 @@ set(SRCS
|
|||
qtutils.cpp
|
||||
qtutils.h
|
||||
resource.h
|
||||
selectdiscdialog.cpp
|
||||
selectdiscdialog.h
|
||||
selectdiscdialog.ui
|
||||
settingswindow.cpp
|
||||
settingswindow.h
|
||||
settingswindow.ui
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
<ClCompile Include="qtkeycodes.cpp" />
|
||||
<ClCompile Include="qtprogresscallback.cpp" />
|
||||
<ClCompile Include="qtutils.cpp" />
|
||||
<ClCompile Include="selectdiscdialog.cpp" />
|
||||
<ClCompile Include="settingswindow.cpp" />
|
||||
<ClCompile Include="setupwizarddialog.cpp" />
|
||||
</ItemGroup>
|
||||
|
@ -88,6 +89,7 @@
|
|||
<QtMoc Include="memoryscannerwindow.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<QtMoc Include="selectdiscdialog.h" />
|
||||
<ClInclude Include="settingwidgetbinder.h" />
|
||||
<QtMoc Include="consolesettingswidget.h" />
|
||||
<QtMoc Include="emulationsettingswidget.h" />
|
||||
|
@ -257,6 +259,7 @@
|
|||
<ClCompile Include="$(IntDir)moc_memoryscannerwindow.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_memoryviewwidget.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_postprocessingsettingswidget.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_selectdiscdialog.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_qthost.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_qtprogresscallback.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_settingswindow.cpp" />
|
||||
|
@ -344,6 +347,9 @@
|
|||
<QtUi Include="controllerbindingwidget_justifier.ui">
|
||||
<FileType>Document</FileType>
|
||||
</QtUi>
|
||||
<QtUi Include="selectdiscdialog.ui">
|
||||
<FileType>Document</FileType>
|
||||
</QtUi>
|
||||
<None Include="translations\duckstation-qt_es-es.ts" />
|
||||
<None Include="translations\duckstation-qt_tr.ts" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -182,6 +182,10 @@
|
|||
<ClCompile Include="$(IntDir)moc_memoryscannerwindow.cpp">
|
||||
<Filter>moc</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="selectdiscdialog.cpp" />
|
||||
<ClCompile Include="$(IntDir)moc_selectdiscdialog.cpp">
|
||||
<Filter>moc</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="qtutils.h" />
|
||||
|
@ -246,6 +250,7 @@
|
|||
<QtMoc Include="logwindow.h" />
|
||||
<QtMoc Include="graphicssettingswidget.h" />
|
||||
<QtMoc Include="memoryscannerwindow.h" />
|
||||
<QtMoc Include="selectdiscdialog.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUi Include="consolesettingswidget.ui" />
|
||||
|
@ -292,6 +297,7 @@
|
|||
<QtUi Include="audioexpansionsettingsdialog.ui" />
|
||||
<QtUi Include="audiostretchsettingsdialog.ui" />
|
||||
<QtUi Include="controllerbindingwidget_justifier.ui" />
|
||||
<QtUi Include="selectdiscdialog.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="qt5.natvis" />
|
||||
|
|
|
@ -38,6 +38,14 @@ class GameListSortModel final : public QSortFilterProxyModel
|
|||
public:
|
||||
explicit GameListSortModel(GameListModel* parent) : QSortFilterProxyModel(parent), m_model(parent) {}
|
||||
|
||||
bool getMergeDiscSets() const { return m_merge_disc_sets; }
|
||||
|
||||
void setMergeDiscSets(bool enabled)
|
||||
{
|
||||
m_merge_disc_sets = enabled;
|
||||
invalidateRowsFilter();
|
||||
}
|
||||
|
||||
void setFilterType(GameList::EntryType type)
|
||||
{
|
||||
m_filter_type = type;
|
||||
|
@ -56,18 +64,28 @@ public:
|
|||
|
||||
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override
|
||||
{
|
||||
if (m_filter_type != GameList::EntryType::Count || m_filter_region != DiscRegion::Count || !m_filter_name.isEmpty())
|
||||
const auto lock = GameList::GetLock();
|
||||
const GameList::Entry* entry = GameList::GetEntryByIndex(source_row);
|
||||
|
||||
if (m_merge_disc_sets)
|
||||
{
|
||||
const auto lock = GameList::GetLock();
|
||||
const GameList::Entry* entry = GameList::GetEntryByIndex(source_row);
|
||||
if (m_filter_type != GameList::EntryType::Count && entry->type != m_filter_type)
|
||||
return false;
|
||||
if (m_filter_region != DiscRegion::Count && entry->region != m_filter_region)
|
||||
return false;
|
||||
if (!m_filter_name.isEmpty() &&
|
||||
!QString::fromStdString(entry->title).contains(m_filter_name, Qt::CaseInsensitive))
|
||||
if (entry->disc_set_member)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (entry->IsDiscSet())
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_filter_type != GameList::EntryType::Count && entry->type != m_filter_type)
|
||||
return false;
|
||||
|
||||
if (m_filter_region != DiscRegion::Count && entry->region != m_filter_region)
|
||||
return false;
|
||||
|
||||
if (!m_filter_name.isEmpty() && !QString::fromStdString(entry->title).contains(m_filter_name, Qt::CaseInsensitive))
|
||||
return false;
|
||||
|
||||
return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
|
||||
}
|
||||
|
@ -82,6 +100,7 @@ private:
|
|||
GameList::EntryType m_filter_type = GameList::EntryType::Count;
|
||||
DiscRegion m_filter_region = DiscRegion::Count;
|
||||
QString m_filter_name;
|
||||
bool m_merge_disc_sets = true;
|
||||
};
|
||||
|
||||
GameListWidget::GameListWidget(QWidget* parent /* = nullptr */) : QWidget(parent)
|
||||
|
@ -94,11 +113,13 @@ void GameListWidget::initialize()
|
|||
{
|
||||
const float cover_scale = Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f);
|
||||
const bool show_cover_titles = Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true);
|
||||
const bool merge_disc_sets = Host::GetBaseBoolSettingValue("UI", "GameListMergeDiscSets", true);
|
||||
m_model = new GameListModel(cover_scale, show_cover_titles, this);
|
||||
m_model->updateCacheSize(width(), height());
|
||||
|
||||
m_sort_model = new GameListSortModel(m_model);
|
||||
m_sort_model->setSourceModel(m_model);
|
||||
m_sort_model->setMergeDiscSets(merge_disc_sets);
|
||||
|
||||
m_ui.setupUi(this);
|
||||
for (u32 type = 0; type < static_cast<u32>(GameList::EntryType::Count); type++)
|
||||
|
@ -117,6 +138,7 @@ void GameListWidget::initialize()
|
|||
connect(m_ui.viewGameGrid, &QPushButton::clicked, this, &GameListWidget::showGameGrid);
|
||||
connect(m_ui.gridScale, &QSlider::valueChanged, this, &GameListWidget::gridIntScale);
|
||||
connect(m_ui.viewGridTitles, &QPushButton::toggled, this, &GameListWidget::setShowCoverTitles);
|
||||
connect(m_ui.viewMergeDiscSets, &QPushButton::toggled, this, &GameListWidget::setMergeDiscSets);
|
||||
connect(m_ui.filterType, &QComboBox::currentIndexChanged, this, [this](int index) {
|
||||
m_sort_model->setFilterType((index == 0) ? GameList::EntryType::Count :
|
||||
static_cast<GameList::EntryType>(index - 1));
|
||||
|
@ -429,6 +451,21 @@ void GameListWidget::setShowCoverTitles(bool enabled)
|
|||
emit layoutChange();
|
||||
}
|
||||
|
||||
void GameListWidget::setMergeDiscSets(bool enabled)
|
||||
{
|
||||
if (m_sort_model->getMergeDiscSets() == enabled)
|
||||
{
|
||||
updateToolbar();
|
||||
return;
|
||||
}
|
||||
|
||||
Host::SetBaseBoolSettingValue("UI", "GameListMergeDiscSets", enabled);
|
||||
Host::CommitBaseSettingChanges();
|
||||
m_sort_model->setMergeDiscSets(enabled);
|
||||
updateToolbar();
|
||||
emit layoutChange();
|
||||
}
|
||||
|
||||
void GameListWidget::updateToolbar()
|
||||
{
|
||||
const bool grid_view = isShowingGameGrid();
|
||||
|
@ -444,6 +481,10 @@ void GameListWidget::updateToolbar()
|
|||
QSignalBlocker sb(m_ui.viewGridTitles);
|
||||
m_ui.viewGridTitles->setChecked(m_model->getShowCoverTitles());
|
||||
}
|
||||
{
|
||||
QSignalBlocker sb(m_ui.viewMergeDiscSets);
|
||||
m_ui.viewMergeDiscSets->setChecked(m_sort_model->getMergeDiscSets());
|
||||
}
|
||||
{
|
||||
QSignalBlocker sb(m_ui.gridScale);
|
||||
m_ui.gridScale->setValue(static_cast<int>(m_model->getCoverScale() * 100.0f));
|
||||
|
|
|
@ -82,6 +82,7 @@ public Q_SLOTS:
|
|||
void showGameList();
|
||||
void showGameGrid();
|
||||
void setShowCoverTitles(bool enabled);
|
||||
void setMergeDiscSets(bool enabled);
|
||||
void gridZoomIn();
|
||||
void gridZoomOut();
|
||||
void gridIntScale(int int_scale);
|
||||
|
|
|
@ -91,6 +91,29 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="viewMergeDiscSets">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Merge Multi-Disc Games</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="play-list-2-line">
|
||||
<normaloff>.</normaloff>.</iconset>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="viewGridTitles">
|
||||
<property name="minimumSize">
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include "memoryscannerwindow.h"
|
||||
#include "qthost.h"
|
||||
#include "qtutils.h"
|
||||
#include "selectdiscdialog.h"
|
||||
#include "settingswindow.h"
|
||||
#include "settingwidgetbinder.h"
|
||||
|
||||
|
@ -1077,6 +1078,22 @@ void MainWindow::populateCheatsMenu(QMenu* menu)
|
|||
}
|
||||
}
|
||||
|
||||
const GameList::Entry* MainWindow::resolveDiscSetEntry(const GameList::Entry* entry,
|
||||
std::unique_lock<std::recursive_mutex>& lock)
|
||||
{
|
||||
if (!entry || entry->type != GameList::EntryType::DiscSet)
|
||||
return entry;
|
||||
|
||||
// disc set... need to figure out the disc we want
|
||||
SelectDiscDialog dlg(entry->path, this);
|
||||
|
||||
lock.unlock();
|
||||
const int res = dlg.exec();
|
||||
lock.lock();
|
||||
|
||||
return res ? GameList::GetEntryForPath(dlg.getSelectedDiscPath()) : nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<SystemBootParameters> MainWindow::getSystemBootParameters(std::string file)
|
||||
{
|
||||
std::shared_ptr<SystemBootParameters> ret = std::make_shared<SystemBootParameters>(std::move(file));
|
||||
|
@ -1376,7 +1393,7 @@ void MainWindow::onGameListSelectionChanged()
|
|||
void MainWindow::onGameListEntryActivated()
|
||||
{
|
||||
auto lock = GameList::GetLock();
|
||||
const GameList::Entry* entry = m_game_list_widget->getSelectedEntry();
|
||||
const GameList::Entry* entry = resolveDiscSetEntry(m_game_list_widget->getSelectedEntry(), lock);
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
|
@ -1421,67 +1438,83 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
|
|||
// Hopefully this pointer doesn't disappear... it shouldn't.
|
||||
if (entry)
|
||||
{
|
||||
QAction* action = menu.addAction(tr("Properties..."));
|
||||
connect(action, &QAction::triggered,
|
||||
[entry]() { SettingsWindow::openGamePropertiesDialog(entry->path, entry->serial, entry->region); });
|
||||
|
||||
connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, entry]() {
|
||||
const QFileInfo fi(QString::fromStdString(entry->path));
|
||||
QtUtils::OpenURL(this, QUrl::fromLocalFile(fi.absolutePath()));
|
||||
});
|
||||
|
||||
connect(menu.addAction(tr("Set Cover Image...")), &QAction::triggered,
|
||||
[this, entry]() { setGameListEntryCoverImage(entry); });
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
if (!s_system_valid)
|
||||
if (!entry->IsDiscSet())
|
||||
{
|
||||
populateGameListContextMenu(entry, this, &menu);
|
||||
connect(menu.addAction(tr("Properties...")), &QAction::triggered,
|
||||
[entry]() { SettingsWindow::openGamePropertiesDialog(entry->path, entry->serial, entry->region); });
|
||||
|
||||
connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, entry]() {
|
||||
const QFileInfo fi(QString::fromStdString(entry->path));
|
||||
QtUtils::OpenURL(this, QUrl::fromLocalFile(fi.absolutePath()));
|
||||
});
|
||||
|
||||
connect(menu.addAction(tr("Set Cover Image...")), &QAction::triggered,
|
||||
[this, entry]() { setGameListEntryCoverImage(entry); });
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
connect(menu.addAction(tr("Default Boot")), &QAction::triggered,
|
||||
[this, entry]() { g_emu_thread->bootSystem(getSystemBootParameters(entry->path)); });
|
||||
|
||||
connect(menu.addAction(tr("Fast Boot")), &QAction::triggered, [this, entry]() {
|
||||
std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
|
||||
boot_params->override_fast_boot = true;
|
||||
g_emu_thread->bootSystem(std::move(boot_params));
|
||||
});
|
||||
|
||||
connect(menu.addAction(tr("Full Boot")), &QAction::triggered, [this, entry]() {
|
||||
std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
|
||||
boot_params->override_fast_boot = false;
|
||||
g_emu_thread->bootSystem(std::move(boot_params));
|
||||
});
|
||||
|
||||
if (m_ui.menuDebug->menuAction()->isVisible() && !Achievements::IsHardcoreModeActive())
|
||||
if (!s_system_valid)
|
||||
{
|
||||
connect(menu.addAction(tr("Boot and Debug")), &QAction::triggered, [this, entry]() {
|
||||
m_open_debugger_on_start = true;
|
||||
populateGameListContextMenu(entry, this, &menu);
|
||||
menu.addSeparator();
|
||||
|
||||
connect(menu.addAction(tr("Default Boot")), &QAction::triggered,
|
||||
[this, entry]() { g_emu_thread->bootSystem(getSystemBootParameters(entry->path)); });
|
||||
|
||||
connect(menu.addAction(tr("Fast Boot")), &QAction::triggered, [this, entry]() {
|
||||
std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
|
||||
boot_params->override_start_paused = true;
|
||||
boot_params->override_fast_boot = true;
|
||||
g_emu_thread->bootSystem(std::move(boot_params));
|
||||
});
|
||||
|
||||
connect(menu.addAction(tr("Full Boot")), &QAction::triggered, [this, entry]() {
|
||||
std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
|
||||
boot_params->override_fast_boot = false;
|
||||
g_emu_thread->bootSystem(std::move(boot_params));
|
||||
});
|
||||
|
||||
if (m_ui.menuDebug->menuAction()->isVisible() && !Achievements::IsHardcoreModeActive())
|
||||
{
|
||||
connect(menu.addAction(tr("Boot and Debug")), &QAction::triggered, [this, entry]() {
|
||||
m_open_debugger_on_start = true;
|
||||
|
||||
std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
|
||||
boot_params->override_start_paused = true;
|
||||
g_emu_thread->bootSystem(std::move(boot_params));
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
connect(menu.addAction(tr("Change Disc")), &QAction::triggered, [this, entry]() {
|
||||
g_emu_thread->changeDisc(QString::fromStdString(entry->path), false, true);
|
||||
g_emu_thread->setSystemPaused(false);
|
||||
switchToEmulationView();
|
||||
});
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
connect(menu.addAction(tr("Exclude From List")), &QAction::triggered,
|
||||
[this, entry]() { getSettingsDialog()->getGameListSettingsWidget()->addExcludedPath(entry->path); });
|
||||
|
||||
connect(menu.addAction(tr("Reset Play Time")), &QAction::triggered,
|
||||
[this, entry]() { clearGameListEntryPlayTime(entry); });
|
||||
}
|
||||
else
|
||||
{
|
||||
connect(menu.addAction(tr("Change Disc")), &QAction::triggered, [this, entry]() {
|
||||
g_emu_thread->changeDisc(QString::fromStdString(entry->path), false, true);
|
||||
g_emu_thread->setSystemPaused(false);
|
||||
switchToEmulationView();
|
||||
connect(menu.addAction(tr("Properties...")), &QAction::triggered, [disc_set_name = entry->path]() {
|
||||
// resolve path first
|
||||
auto lock = GameList::GetLock();
|
||||
const GameList::Entry* first_disc = GameList::GetFirstDiscSetMember(disc_set_name);
|
||||
if (first_disc)
|
||||
SettingsWindow::openGamePropertiesDialog(first_disc->path, first_disc->serial, first_disc->region);
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
connect(menu.addAction(tr("Select Disc")), &QAction::triggered, this, &MainWindow::onGameListEntryActivated);
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
connect(menu.addAction(tr("Exclude From List")), &QAction::triggered,
|
||||
[this, entry]() { getSettingsDialog()->getGameListSettingsWidget()->addExcludedPath(entry->path); });
|
||||
|
||||
connect(menu.addAction(tr("Reset Play Time")), &QAction::triggered,
|
||||
[this, entry]() { clearGameListEntryPlayTime(entry); });
|
||||
}
|
||||
|
||||
connect(menu.addAction(tr("Add Search Directory...")), &QAction::triggered,
|
||||
|
@ -2037,6 +2070,7 @@ void MainWindow::connectSignals()
|
|||
connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableGDBServer, "Debug", "EnableGDBServer", false);
|
||||
connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered);
|
||||
connect(m_ui.actionMergeDiscSets, &QAction::triggered, m_game_list_widget, &GameListWidget::setMergeDiscSets);
|
||||
connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
|
||||
connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() {
|
||||
if (isShowingGameList())
|
||||
|
|
|
@ -270,6 +270,8 @@ private:
|
|||
/// Fills menu with the current cheat options.
|
||||
void populateCheatsMenu(QMenu* menu);
|
||||
|
||||
const GameList::Entry* resolveDiscSetEntry(const GameList::Entry* entry,
|
||||
std::unique_lock<std::recursive_mutex>& lock);
|
||||
std::shared_ptr<SystemBootParameters> getSystemBootParameters(std::string file);
|
||||
std::optional<bool> promptForResumeState(const std::string& save_state_path);
|
||||
void startFile(std::string path, std::optional<std::string> save_path, std::optional<bool> fast_boot);
|
||||
|
|
|
@ -220,6 +220,7 @@
|
|||
<addaction name="actionFullscreen"/>
|
||||
<addaction name="menuWindowSize"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionMergeDiscSets" />
|
||||
<addaction name="actionGridViewShowTitles"/>
|
||||
<addaction name="actionGridViewZoomIn"/>
|
||||
<addaction name="actionGridViewZoomOut"/>
|
||||
|
@ -863,6 +864,17 @@
|
|||
<string>Game &Grid</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionMergeDiscSets">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Merge Multi-Disc Games</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionGridViewShowTitles">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
|
|
|
@ -304,6 +304,7 @@ QIcon GetIconForEntryType(GameList::EntryType type)
|
|||
case GameList::EntryType::Disc:
|
||||
return QIcon::fromTheme(QStringLiteral("disc-line"));
|
||||
case GameList::EntryType::Playlist:
|
||||
case GameList::EntryType::DiscSet:
|
||||
return QIcon::fromTheme(QStringLiteral("play-list-2-line"));
|
||||
case GameList::EntryType::PSF:
|
||||
return QIcon::fromTheme(QStringLiteral("file-music-line"));
|
||||
|
|
89
src/duckstation-qt/selectdiscdialog.cpp
Normal file
89
src/duckstation-qt/selectdiscdialog.cpp
Normal file
|
@ -0,0 +1,89 @@
|
|||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||
|
||||
#include "selectdiscdialog.h"
|
||||
#include "qtutils.h"
|
||||
|
||||
#include "core/game_list.h"
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/path.h"
|
||||
|
||||
#include <QtWidgets/QTreeWidget>
|
||||
|
||||
SelectDiscDialog::SelectDiscDialog(const std::string& disc_set_name, QWidget* parent /* = nullptr */) : QDialog(parent)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
populateList(disc_set_name);
|
||||
updateStartEnabled();
|
||||
|
||||
connect(m_ui.select, &QPushButton::clicked, this, &SelectDiscDialog::onSelectClicked);
|
||||
connect(m_ui.cancel, &QPushButton::clicked, this, &SelectDiscDialog::onCancelClicked);
|
||||
connect(m_ui.discList, &QTreeWidget::itemActivated, this, &SelectDiscDialog::onListItemActivated);
|
||||
connect(m_ui.discList, &QTreeWidget::itemSelectionChanged, this, &SelectDiscDialog::updateStartEnabled);
|
||||
}
|
||||
|
||||
SelectDiscDialog::~SelectDiscDialog() = default;
|
||||
|
||||
void SelectDiscDialog::resizeEvent(QResizeEvent* ev)
|
||||
{
|
||||
QDialog::resizeEvent(ev);
|
||||
|
||||
QtUtils::ResizeColumnsForTreeView(m_ui.discList, {50, -1, 100});
|
||||
}
|
||||
|
||||
void SelectDiscDialog::onListItemActivated(const QTreeWidgetItem* item)
|
||||
{
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
m_selected_path = item->data(0, Qt::UserRole).toString().toStdString();
|
||||
done(1);
|
||||
}
|
||||
|
||||
void SelectDiscDialog::updateStartEnabled()
|
||||
{
|
||||
const QList<QTreeWidgetItem*> items = m_ui.discList->selectedItems();
|
||||
m_ui.select->setEnabled(!items.isEmpty());
|
||||
if (!items.isEmpty())
|
||||
m_selected_path = items.first()->data(0, Qt::UserRole).toString().toStdString();
|
||||
else
|
||||
m_selected_path = {};
|
||||
}
|
||||
|
||||
void SelectDiscDialog::onSelectClicked()
|
||||
{
|
||||
done(1);
|
||||
}
|
||||
|
||||
void SelectDiscDialog::onCancelClicked()
|
||||
{
|
||||
done(0);
|
||||
}
|
||||
|
||||
void SelectDiscDialog::populateList(const std::string& disc_set_name)
|
||||
{
|
||||
const auto lock = GameList::GetLock();
|
||||
const std::vector<const GameList::Entry*> entries = GameList::GetDiscSetMembers(disc_set_name);
|
||||
const GameList::Entry* last_played_entry = nullptr;
|
||||
|
||||
for (const GameList::Entry* entry : entries)
|
||||
{
|
||||
QTreeWidgetItem* item = new QTreeWidgetItem();
|
||||
item->setData(0, Qt::UserRole, QString::fromStdString(entry->path));
|
||||
item->setIcon(0, QtUtils::GetIconForEntryType(GameList::EntryType::Disc));
|
||||
item->setText(0, QString::number(entry->disc_set_index + 1));
|
||||
item->setText(1, QtUtils::StringViewToQString(Path::GetFileName(entry->path)));
|
||||
item->setText(2, QtUtils::StringViewToQString(GameList::FormatTimestamp(entry->last_played_time)));
|
||||
m_ui.discList->addTopLevelItem(item);
|
||||
|
||||
if (!last_played_entry ||
|
||||
(entry->last_played_time > 0 && entry->last_played_time > last_played_entry->last_played_time))
|
||||
{
|
||||
last_played_entry = entry;
|
||||
m_ui.discList->setCurrentItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
setWindowTitle(tr("Select Disc for %1").arg(QString::fromStdString(disc_set_name)));
|
||||
}
|
38
src/duckstation-qt/selectdiscdialog.h
Normal file
38
src/duckstation-qt/selectdiscdialog.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||
|
||||
#pragma once
|
||||
#include "common/timer.h"
|
||||
#include "common/types.h"
|
||||
#include "qtprogresscallback.h"
|
||||
#include "ui_selectdiscdialog.h"
|
||||
#include <QtWidgets/QDialog>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class SelectDiscDialog final : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SelectDiscDialog(const std::string& disc_set_name, QWidget* parent = nullptr);
|
||||
~SelectDiscDialog();
|
||||
|
||||
ALWAYS_INLINE const std::string& getSelectedDiscPath() { return m_selected_path; }
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* ev);
|
||||
|
||||
private Q_SLOTS:
|
||||
void onListItemActivated(const QTreeWidgetItem* item);
|
||||
void updateStartEnabled();
|
||||
void onSelectClicked();
|
||||
void onCancelClicked();
|
||||
|
||||
private:
|
||||
void populateList(const std::string& disc_set_name);
|
||||
|
||||
Ui::SelectDiscDialog m_ui;
|
||||
std::string m_selected_path;
|
||||
};
|
84
src/duckstation-qt/selectdiscdialog.ui
Normal file
84
src/duckstation-qt/selectdiscdialog.ui
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SelectDiscDialog</class>
|
||||
<widget class="QDialog" name="SelectDiscDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>553</width>
|
||||
<height>206</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Select the disc that you want to boot.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="discList">
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Disc</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>File Name</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Last Played</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="select">
|
||||
<property name="text">
|
||||
<string>Select</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancel">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
Loading…
Reference in a new issue