GameList: Add custom title/regions

Largely inspired by https://github.com/PCSX2/pcsx2/pull/9330, but almost
entirely rewritten.
This commit is contained in:
Stenzek 2024-06-17 23:25:08 +10:00
parent 01fc3258a3
commit ccb76d1451
No known key found for this signature in database
12 changed files with 372 additions and 55 deletions

View File

@ -11,6 +11,7 @@
#include "util/cd_image.h"
#include "util/http_downloader.h"
#include "util/ini_settings_interface.h"
#include "common/assert.h"
#include "common/byte_stream.h"
@ -69,13 +70,19 @@ static bool GetExeListEntry(const std::string& path, Entry* entry);
static bool GetPsfListEntry(const std::string& path, Entry* entry);
static bool GetDiscListEntry(const std::string& path, Entry* entry);
static bool GetGameListEntryFromCache(const std::string& path, Entry* entry);
static void ApplyCustomAttributes(const std::string& path, Entry* entry,
const INISettingsInterface& custom_attributes_ini);
static bool RescanCustomAttributesForPath(const std::string& path, const INISettingsInterface& custom_attributes_ini);
static bool GetGameListEntryFromCache(const std::string& path, Entry* entry,
const INISettingsInterface& custom_attributes_ini);
static Entry* GetMutableEntryForPath(std::string_view path);
static void ScanDirectory(const char* path, bool recursive, bool only_cache,
const std::vector<std::string>& excluded_paths, const PlayedTimeMap& played_time_map,
ProgressCallback* progress);
static bool AddFileFromCache(const std::string& path, std::time_t timestamp, 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,
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 PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini);
static std::string GetCacheFilename();
static void LoadCache();
@ -92,6 +99,8 @@ static std::string MakePlayedTimeLine(const std::string& serial, const PlayedTim
static PlayedTimeMap LoadPlayedTimeMap(const std::string& path);
static PlayedTimeEntry UpdatePlayedTimeFile(const std::string& path, const std::string& serial, std::time_t last_time,
std::time_t add_time);
static std::string GetCustomPropertiesFile();
} // namespace GameList
static std::vector<GameList::Entry> s_entries;
@ -307,7 +316,8 @@ bool GameList::PopulateEntryFromPath(const std::string& path, Entry* entry)
return GetDiscListEntry(path, entry);
}
bool GameList::GetGameListEntryFromCache(const std::string& path, Entry* entry)
bool GameList::GetGameListEntryFromCache(const std::string& path, Entry* entry,
const INISettingsInterface& custom_attributes_ini)
{
auto iter = s_cache_map.find(path);
if (iter == s_cache_map.end())
@ -315,6 +325,7 @@ bool GameList::GetGameListEntryFromCache(const std::string& path, Entry* entry)
*entry = std::move(iter->second);
s_cache_map.erase(iter);
ApplyCustomAttributes(path, entry, custom_attributes_ini);
return true;
}
@ -491,7 +502,7 @@ static bool IsPathExcluded(const std::vector<std::string>& excluded_paths, const
void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
const std::vector<std::string>& excluded_paths, const PlayedTimeMap& played_time_map,
ProgressCallback* progress)
const INISettingsInterface& custom_attributes_ini, ProgressCallback* progress)
{
INFO_LOG("Scanning {}{}", path, recursive ? " (recursively)" : "");
@ -521,15 +532,15 @@ 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, custom_attributes_ini) || only_cache)
{
continue;
}
progress->SetStatusText(SmallString::from_format(TRANSLATE_FS("GameList", "Scanning '{}'..."),
FileSystem::GetDisplayNameFromPath(ffd.FileName)));
ScanFile(std::move(ffd.FileName), ffd.ModificationTime, lock, played_time_map);
ScanFile(std::move(ffd.FileName), ffd.ModificationTime, lock, played_time_map, custom_attributes_ini);
progress->SetProgressValue(files_scanned);
}
@ -537,10 +548,11 @@ 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)
if (!GetGameListEntryFromCache(path, &entry, custom_attributes_ini) || entry.last_modified_time != timestamp)
return false;
auto iter = played_time_map.find(entry.serial);
@ -555,7 +567,7 @@ bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp,
}
bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock,
const PlayedTimeMap& played_time_map)
const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini)
{
// don't block UI while scanning
lock.unlock();
@ -575,18 +587,97 @@ bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_loc
WARNING_LOG("Failed to write entry '{}' to cache", entry.path);
}
auto iter = played_time_map.find(entry.serial);
const auto iter = played_time_map.find(entry.serial);
if (iter != played_time_map.end())
{
entry.last_played_time = iter->second.last_played_time;
entry.total_played_time = iter->second.total_played_time;
}
ApplyCustomAttributes(path, &entry, custom_attributes_ini);
lock.lock();
s_entries.push_back(std::move(entry));
// replace if present
auto it = std::find_if(s_entries.begin(), s_entries.end(),
[&entry](const Entry& existing_entry) { return (existing_entry.path == entry.path); });
if (it != s_entries.end())
*it = std::move(entry);
else
s_entries.push_back(std::move(entry));
return true;
}
bool GameList::RescanCustomAttributesForPath(const std::string& path, const INISettingsInterface& custom_attributes_ini)
{
FILESYSTEM_STAT_DATA sd;
if (!FileSystem::StatFile(path.c_str(), &sd))
return false;
{
// cancel if excluded
const std::vector<std::string> excluded_paths(Host::GetBaseStringListSetting("GameList", "ExcludedPaths"));
if (IsPathExcluded(excluded_paths, path))
return false;
}
Entry entry;
if (!PopulateEntryFromPath(path, &entry))
return false;
entry.path = std::move(path);
entry.last_modified_time = sd.ModificationTime;
const PlayedTimeMap played_time_map(LoadPlayedTimeMap(GetPlayedTimeFile()));
const auto iter = played_time_map.find(entry.serial);
if (iter != played_time_map.end())
{
entry.last_played_time = iter->second.last_played_time;
entry.total_played_time = iter->second.total_played_time;
}
ApplyCustomAttributes(path, &entry, custom_attributes_ini);
std::unique_lock lock(s_mutex);
// replace if present
auto it = std::find_if(s_entries.begin(), s_entries.end(),
[&entry](const Entry& existing_entry) { return (existing_entry.path == entry.path); });
if (it != s_entries.end())
*it = std::move(entry);
else
s_entries.push_back(std::move(entry));
return true;
}
void GameList::ApplyCustomAttributes(const std::string& path, Entry* entry,
const INISettingsInterface& custom_attributes_ini)
{
std::optional<std::string> custom_title = custom_attributes_ini.GetOptionalStringValue(path.c_str(), "Title");
if (custom_title.has_value())
{
entry->title = std::move(custom_title.value());
entry->has_custom_title = true;
}
const std::optional<SmallString> custom_region_str =
custom_attributes_ini.GetOptionalSmallStringValue(path.c_str(), "Region");
if (custom_region_str.has_value())
{
const std::optional<DiscRegion> custom_region = Settings::ParseDiscRegionName(custom_region_str.value());
if (custom_region.has_value())
{
entry->region = custom_region.value();
entry->has_custom_region = true;
}
else
{
WARNING_LOG("Invalid region '{}' in custom attributes for '{}'", custom_region_str.value(), path);
}
}
}
std::unique_lock<std::recursive_mutex> GameList::GetLock()
{
return std::unique_lock<std::recursive_mutex>(s_mutex);
@ -599,7 +690,12 @@ const GameList::Entry* GameList::GetEntryByIndex(u32 index)
const GameList::Entry* GameList::GetEntryForPath(std::string_view path)
{
for (const Entry& entry : s_entries)
return GetMutableEntryForPath(path);
}
GameList::Entry* GameList::GetMutableEntryForPath(std::string_view path)
{
for (Entry& entry : s_entries)
{
// Use case-insensitive compare on Windows, since it's the same file.
#ifdef _WIN32
@ -709,6 +805,8 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
const std::vector<std::string> dirs(Host::GetBaseStringListSetting("GameList", "Paths"));
std::vector<std::string> recursive_dirs(Host::GetBaseStringListSetting("GameList", "RecursivePaths"));
const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile()));
INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile());
custom_attributes_ini.Load();
#ifdef __ANDROID__
recursive_dirs.push_back(Path::Combine(EmuFolders::DataRoot, "games"));
@ -726,7 +824,7 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
if (progress->IsCancelled())
break;
ScanDirectory(dir.c_str(), false, only_cache, excluded_paths, played_time, progress);
ScanDirectory(dir.c_str(), false, only_cache, excluded_paths, played_time, custom_attributes_ini, progress);
progress->SetProgressValue(++directory_counter);
}
for (const std::string& dir : recursive_dirs)
@ -734,7 +832,7 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
if (progress->IsCancelled())
break;
ScanDirectory(dir.c_str(), true, only_cache, excluded_paths, played_time, progress);
ScanDirectory(dir.c_str(), true, only_cache, excluded_paths, played_time, custom_attributes_ini, progress);
progress->SetProgressValue(++directory_counter);
}
}
@ -1425,3 +1523,109 @@ bool GameList::DownloadCovers(const std::vector<std::string>& url_templates, boo
return true;
}
std::string GameList::GetCustomPropertiesFile()
{
return Path::Combine(EmuFolders::DataRoot, "custom_properties.ini");
}
void GameList::SaveCustomTitleForPath(const std::string& path, const std::string& custom_title)
{
INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile());
custom_attributes_ini.Load();
if (!custom_title.empty())
{
custom_attributes_ini.SetStringValue(path.c_str(), "Title", custom_title.c_str());
}
else
{
custom_attributes_ini.DeleteValue(path.c_str(), "Title");
custom_attributes_ini.RemoveEmptySections();
}
Error error;
if (!custom_attributes_ini.Save(&error))
{
ERROR_LOG("Failed to save custom attributes: {}", error.GetDescription());
return;
}
if (!custom_title.empty())
{
// Can skip the rescan and just update the value directly.
auto lock = GetLock();
Entry* entry = GetMutableEntryForPath(path);
if (entry)
{
entry->title = custom_title;
entry->has_custom_title = true;
}
}
else
{
// Let the cache update by rescanning. Only need to do this on deletion, to get the original value.
RescanCustomAttributesForPath(path, custom_attributes_ini);
}
}
void GameList::SaveCustomRegionForPath(const std::string& path, const std::optional<DiscRegion> custom_region)
{
INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile());
custom_attributes_ini.Load();
if (custom_region.has_value())
{
custom_attributes_ini.SetStringValue(path.c_str(), "Region", Settings::GetDiscRegionName(custom_region.value()));
}
else
{
custom_attributes_ini.DeleteValue(path.c_str(), "Region");
custom_attributes_ini.RemoveEmptySections();
}
Error error;
if (!custom_attributes_ini.Save(&error))
{
ERROR_LOG("Failed to save custom attributes: {}", error.GetDescription());
return;
}
if (custom_region.has_value())
{
// Can skip the rescan and just update the value directly.
auto lock = GetLock();
Entry* entry = GetMutableEntryForPath(path);
if (entry)
{
entry->region = custom_region.value();
entry->has_custom_region = true;
}
}
else
{
// Let the cache update by rescanning. Only need to do this on deletion, to get the original value.
RescanCustomAttributesForPath(path, custom_attributes_ini);
}
}
std::string GameList::GetCustomTitleForPath(const std::string_view path)
{
std::string ret;
std::unique_lock lock(s_mutex);
const GameList::Entry* entry = GetEntryForPath(path);
if (entry && entry->has_custom_title)
ret = entry->title;
return ret;
}
std::optional<DiscRegion> GameList::GetCustomRegionForPath(const std::string_view path)
{
const GameList::Entry* entry = GetEntryForPath(path);
if (entry && entry->has_custom_region)
return entry->region;
else
return std::nullopt;
}

View File

@ -59,6 +59,8 @@ struct Entry
u8 max_blocks = 0;
s8 disc_set_index = -1;
bool disc_set_member = false;
bool has_custom_title = false;
bool has_custom_region = false;
GameDatabase::CompatibilityRating compatibility = GameDatabase::CompatibilityRating::Unknown;
@ -122,6 +124,12 @@ GetMatchingEntriesForSerial(const std::span<const std::string> serials);
bool DownloadCovers(const std::vector<std::string>& url_templates, bool use_serial = false,
ProgressCallback* progress = nullptr,
std::function<void(const Entry*, std::string)> save_callback = {});
// Custom properties support
void SaveCustomTitleForPath(const std::string& path, const std::string& custom_title);
void SaveCustomRegionForPath(const std::string& path, const std::optional<DiscRegion> custom_region);
std::string GetCustomTitleForPath(const std::string_view path);
std::optional<DiscRegion> GetCustomRegionForPath(const std::string_view path);
}; // namespace GameList
namespace Host {

View File

@ -186,7 +186,8 @@ static std::string s_running_game_serial;
static std::string s_running_game_title;
static const GameDatabase::Entry* s_running_game_entry = nullptr;
static System::GameHash s_running_game_hash;
static bool s_was_fast_booted;
static bool s_running_game_custom_title = false;
static bool s_was_fast_booted = false;
static bool s_system_executing = false;
static bool s_system_interrupted = false;
@ -988,20 +989,6 @@ DiscRegion System::GetRegionForPsf(const char* path)
return psf.GetRegion();
}
std::optional<DiscRegion> System::GetRegionForPath(const char* image_path)
{
if (IsExeFileName(image_path))
return GetRegionForExe(image_path);
else if (IsPsfFileName(image_path))
return GetRegionForPsf(image_path);
std::unique_ptr<CDImage> cdi = CDImage::Open(image_path, false, nullptr);
if (!cdi)
return {};
return GetRegionForImage(cdi.get());
}
std::string System::GetGameSettingsPath(std::string_view game_serial)
{
// multi-disc games => always use the first disc
@ -1507,7 +1494,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
return false;
}
disc_region = GetRegionForImage(disc.get());
disc_region = GameList::GetCustomRegionForPath(parameters.filename).value_or(GetRegionForImage(disc.get()));
if (s_region == ConsoleRegion::Auto)
{
if (disc_region != DiscRegion::Other)
@ -2583,7 +2570,8 @@ bool System::LoadStateFromStream(ByteStream* state, Error* error, bool update_di
CDROM::Reset();
if (media)
{
const DiscRegion region = GetRegionForImage(media.get());
const DiscRegion region =
GameList::GetCustomRegionForPath(media_filename).value_or(GetRegionForImage(media.get()));
CDROM::InsertMedia(std::move(media), region);
if (g_settings.cdrom_load_image_to_ram)
CDROM::PrecacheMedia();
@ -3385,7 +3373,9 @@ std::unique_ptr<MemoryCard> System::GetMemoryCardForSlot(u32 slot, MemoryCardTyp
// But prefer a disc-specific card if one already exists.
std::string disc_card_path = g_settings.GetGameMemoryCardPath(
Path::SanitizeFileName(s_running_game_entry ? s_running_game_entry->title : s_running_game_title), slot);
Path::SanitizeFileName((s_running_game_entry && !s_running_game_custom_title) ? s_running_game_entry->title :
s_running_game_title),
slot);
if (disc_card_path != card_path)
{
if (card_path.empty() || !g_settings.memory_card_use_playlist_title ||
@ -3627,7 +3617,7 @@ bool System::InsertMedia(const char* path)
return false;
}
const DiscRegion region = GetRegionForImage(image.get());
const DiscRegion region = GameList::GetCustomRegionForPath(path).value_or(GetRegionForImage(image.get()));
UpdateRunningGame(path, image.get(), false);
CDROM::InsertMedia(std::move(image), region);
INFO_LOG("Inserted media from {} ({}, {})", s_running_game_path, s_running_game_serial, s_running_game_title);
@ -3668,14 +3658,19 @@ void System::UpdateRunningGame(const char* path, CDImage* image, bool booting)
s_running_game_title.clear();
s_running_game_entry = nullptr;
s_running_game_hash = 0;
s_running_game_custom_title = false;
if (path && std::strlen(path) > 0)
{
s_running_game_path = path;
s_running_game_title = GameList::GetCustomTitleForPath(s_running_game_path);
s_running_game_custom_title = !s_running_game_title.empty();
if (IsExeFileName(path))
{
s_running_game_title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path));
if (s_running_game_title.empty())
s_running_game_title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path));
s_running_game_hash = GetGameHashFromFile(path);
if (s_running_game_hash != 0)
s_running_game_serial = GetGameHashId(s_running_game_hash);
@ -3683,7 +3678,8 @@ void System::UpdateRunningGame(const char* path, CDImage* image, bool booting)
else if (IsPsfFileName(path))
{
// TODO: We could pull the title from the PSF.
s_running_game_title = Path::GetFileTitle(path);
if (s_running_game_title.empty())
s_running_game_title = Path::GetFileTitle(path);
}
// Check for an audio CD. Those shouldn't set any title.
else if (image && image->GetTrack(1).mode != CDImage::TrackMode::Audio)
@ -3695,19 +3691,24 @@ void System::UpdateRunningGame(const char* path, CDImage* image, bool booting)
if (s_running_game_entry)
{
s_running_game_serial = s_running_game_entry->serial;
s_running_game_title = s_running_game_entry->title;
if (s_running_game_title.empty())
s_running_game_title = s_running_game_entry->title;
}
else
{
s_running_game_serial = std::move(id);
s_running_game_title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path));
if (s_running_game_title.empty())
s_running_game_title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path));
}
if (image->HasSubImages())
{
std::string image_title = image->GetMetadata("title");
if (!image_title.empty())
{
s_running_game_title = std::move(image_title);
s_running_game_custom_title = false;
}
}
}
}

View File

@ -140,7 +140,6 @@ DiscRegion GetRegionFromSystemArea(CDImage* cdi);
DiscRegion GetRegionForImage(CDImage* cdi);
DiscRegion GetRegionForExe(const char* path);
DiscRegion GetRegionForPsf(const char* path);
std::optional<DiscRegion> GetRegionForPath(const char* image_path);
/// Returns the path for the game settings ini file for the specified serial.
std::string GetGameSettingsPath(std::string_view game_serial);

View File

@ -254,6 +254,11 @@ void GameListWidget::refresh(bool invalidate_cache)
m_refresh_thread->start();
}
void GameListWidget::refreshModel()
{
m_model->refresh();
}
void GameListWidget::cancelRefresh()
{
if (!m_refresh_thread)

View File

@ -45,6 +45,7 @@ public:
void resizeTableViewColumnsToFit();
void refresh(bool invalidate_cache);
void refreshModel();
void cancelRefresh();
void reloadThemeSpecificImages();

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "gamesummarywidget.h"
#include "mainwindow.h"
#include "qthost.h"
#include "qtprogresscallback.h"
#include "settingswindow.h"
@ -54,6 +55,17 @@ GameSummaryWidget::GameSummaryWidget(const std::string& path, const std::string&
connect(m_ui.compatibilityComments, &QToolButton::clicked, this, &GameSummaryWidget::onCompatibilityCommentsClicked);
connect(m_ui.inputProfile, &QComboBox::currentIndexChanged, this, &GameSummaryWidget::onInputProfileChanged);
connect(m_ui.computeHashes, &QAbstractButton::clicked, this, &GameSummaryWidget::onComputeHashClicked);
connect(m_ui.title, &QLineEdit::editingFinished, this, [this]() {
if (m_ui.title->isModified())
{
setCustomTitle(m_ui.title->text().toStdString());
m_ui.title->setModified(false);
}
});
connect(m_ui.restoreTitle, &QAbstractButton::clicked, this, [this]() { setCustomTitle(std::string()); });
connect(m_ui.region, &QComboBox::currentIndexChanged, this, [this](int index) { setCustomRegion(index); });
connect(m_ui.restoreRegion, &QAbstractButton::clicked, this, [this]() { setCustomRegion(-1); });
}
GameSummaryWidget::~GameSummaryWidget() = default;
@ -157,7 +169,56 @@ void GameSummaryWidget::populateUi(const std::string& path, const std::string& s
else
m_ui.inputProfile->setCurrentIndex(0);
populateCustomAttributes();
populateTracksInfo();
updateWindowTitle();
}
void GameSummaryWidget::populateCustomAttributes()
{
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(m_path);
if (!entry || entry->IsDiscSet())
return;
{
QSignalBlocker sb(m_ui.title);
m_ui.title->setText(QString::fromStdString(entry->title));
m_ui.restoreTitle->setEnabled(entry->has_custom_title);
}
{
QSignalBlocker sb(m_ui.region);
m_ui.region->setCurrentIndex(static_cast<int>(entry->region));
m_ui.restoreRegion->setEnabled(entry->has_custom_region);
}
}
void GameSummaryWidget::updateWindowTitle()
{
const QString window_title = tr("%1 [%2]").arg(m_ui.title->text()).arg(m_ui.serial->text());
m_dialog->setWindowTitle(window_title);
}
void GameSummaryWidget::setCustomTitle(const std::string& text)
{
m_ui.restoreTitle->setEnabled(!text.empty());
GameList::SaveCustomTitleForPath(m_path, text);
populateCustomAttributes();
updateWindowTitle();
g_main_window->refreshGameListModel();
}
void GameSummaryWidget::setCustomRegion(int region)
{
m_ui.restoreRegion->setEnabled(region >= 0);
GameList::SaveCustomRegionForPath(m_path, (region >= 0) ? std::optional<DiscRegion>(static_cast<DiscRegion>(region)) :
std::optional<DiscRegion>());
populateCustomAttributes();
updateWindowTitle();
g_main_window->refreshGameListModel();
}
static QString MSFTotString(const CDImage::Position& position)

View File

@ -32,6 +32,11 @@ private Q_SLOTS:
private:
void populateUi(const std::string& path, const std::string& serial, DiscRegion region,
const GameDatabase::Entry* entry);
void populateCustomAttributes();
void updateWindowTitle();
void setCustomTitle(const std::string& text);
void setCustomRegion(int region);
void populateTracksInfo();
Ui::GameSummaryWidget m_ui;

View File

@ -38,18 +38,49 @@
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="title">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLineEdit" name="title">
<property name="placeholderText">
<string>Clear the line to restore the original title...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="restoreTitle">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Restore</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="region">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QComboBox" name="region">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="restoreRegion">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Restore</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">

View File

@ -2821,6 +2821,11 @@ void MainWindow::refreshGameList(bool invalidate_cache)
m_game_list_widget->refresh(invalidate_cache);
}
void MainWindow::refreshGameListModel()
{
m_game_list_widget->refreshModel();
}
void MainWindow::cancelGameListRefresh()
{
m_game_list_widget->cancelRefresh();

View File

@ -107,6 +107,7 @@ public Q_SLOTS:
void updateDebugMenuVisibility();
void refreshGameList(bool invalidate_cache);
void refreshGameListModel();
void cancelGameListRefresh();
void runOnUIThread(const std::function<void()>& func);

View File

@ -653,11 +653,7 @@ void SettingsWindow::openGamePropertiesDialog(const std::string& path, const std
if (FileSystem::FileExists(sif->GetFileName().c_str()))
sif->Load();
const QString window_title(
tr("%1 [%2]").arg(QString::fromStdString(dentry ? dentry->title : title)).arg(QString::fromStdString(real_serial)));
SettingsWindow* dialog = new SettingsWindow(path, real_serial, region, dentry, std::move(sif));
dialog->setWindowTitle(window_title);
dialog->show();
}