Skip to content

Commit

Permalink
Qt: Add game list language override option
Browse files Browse the repository at this point in the history
  • Loading branch information
stenzek committed Nov 24, 2024
1 parent 70a4b5c commit 852239e
Show file tree
Hide file tree
Showing 11 changed files with 310 additions and 196 deletions.
2 changes: 1 addition & 1 deletion src/core/fullscreen_ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6882,7 +6882,7 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
const bool display_as_language = (selected_entry->dbentry && selected_entry->dbentry->HasAnyLanguage());
ImGui::TextUnformatted(display_as_language ? FSUI_CSTR("Language: ") : FSUI_CSTR("Region: "));
ImGui::SameLine();
ImGui::Image(GetCachedTexture(selected_entry->GetLanguageIconFileName(), 23, 16), LayoutScale(23.0f, 16.0f));
ImGui::Image(GetCachedTexture(selected_entry->GetLanguageIconName(), 23, 16), LayoutScale(23.0f, 16.0f));
ImGui::SameLine();
if (display_as_language)
{
Expand Down
19 changes: 19 additions & 0 deletions src/core/game_database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

#include "ryml.hpp"

#include <bit>
#include <iomanip>
#include <memory>
#include <optional>
Expand Down Expand Up @@ -312,6 +313,24 @@ std::optional<GameDatabase::Language> GameDatabase::ParseLanguageName(std::strin
return std::nullopt;
}

TinyString GameDatabase::GetLanguageFlagResourceName(std::string_view language_name)
{
return TinyString::from_format("images/flags/{}.svg", language_name);
}

std::string_view GameDatabase::Entry::GetLanguageFlagName(DiscRegion region) const
{
// If there's only one language, this is the flag we want to use.
// Except if it's English, then we want to use the disc region's flag.
std::string_view ret;
if (languages.count() == 1 && !languages.test(static_cast<size_t>(GameDatabase::Language::English)))
ret = GameDatabase::GetLanguageName(static_cast<GameDatabase::Language>(std::countr_zero(languages.to_ulong())));
else
ret = Settings::GetDiscRegionName(region);

return ret;
}

SmallString GameDatabase::Entry::GetLanguagesString() const
{
SmallString ret;
Expand Down
4 changes: 3 additions & 1 deletion src/core/game_database.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@ struct Entry

ALWAYS_INLINE bool HasTrait(Trait trait) const { return traits[static_cast<int>(trait)]; }
ALWAYS_INLINE bool HasLanguage(Language language) const { return languages.test(static_cast<size_t>(language)); }
ALWAYS_INLINE bool HasAnyLanguage() const { return !languages.none(); }
ALWAYS_INLINE bool HasAnyLanguage() const { return languages.any(); }

std::string_view GetLanguageFlagName(DiscRegion region) const;
SmallString GetLanguagesString() const;

void ApplySettings(Settings& settings, bool display_osd_messages) const;
Expand All @@ -156,6 +157,7 @@ const char* GetCompatibilityRatingDisplayName(CompatibilityRating rating);

const char* GetLanguageName(Language language);
std::optional<Language> ParseLanguageName(std::string_view str);
TinyString GetLanguageFlagResourceName(std::string_view language_name);

/// Map of track hashes for image verification
struct TrackData
Expand Down
108 changes: 70 additions & 38 deletions src/core/game_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ static PlayedTimeEntry UpdatePlayedTimeFile(const std::string& path, const std::
std::time_t add_time);

static std::string GetCustomPropertiesFile();
static bool PutCustomPropertiesField(INISettingsInterface& ini, const std::string& path, const char* field,
const char* value);

static FileSystem::ManagedCFilePtr OpenMemoryCardTimestampCache(bool for_write);
static bool UpdateMemcardTimestampCache(const MemcardTimestampCacheEntry& entry);
Expand Down Expand Up @@ -627,6 +629,21 @@ void GameList::ApplyCustomAttributes(const std::string& path, Entry* entry,
WARNING_LOG("Invalid region '{}' in custom attributes for '{}'", custom_region_str.value(), path);
}
}
const std::optional<TinyString> custom_language_str =
custom_attributes_ini.GetOptionalTinyStringValue(path.c_str(), "Language");
if (custom_language_str.has_value())
{
const std::optional<GameDatabase::Language> custom_region =
GameDatabase::ParseLanguageName(custom_region_str.value());
if (custom_region.has_value())
{
entry->custom_language = custom_region.value();
}
else
{
WARNING_LOG("Invalid language '{}' in custom attributes for '{}'", custom_region_str.value(), path);
}
}
}

std::unique_lock<std::recursive_mutex> GameList::GetLock()
Expand Down Expand Up @@ -990,26 +1007,20 @@ std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const cha

std::string_view GameList::Entry::GetLanguageIcon() const
{
// If there's only one language, this is the flag we want to use.
// Except if it's English, then we want to use the disc region's flag.
std::string_view ret;
if (dbentry && dbentry->languages.count() == 1 &&
!dbentry->languages.test(static_cast<size_t>(GameDatabase::Language::English)))
{
ret = GameDatabase::GetLanguageName(
static_cast<GameDatabase::Language>(std::countr_zero(dbentry->languages.to_ulong())));
}
if (custom_language != GameDatabase::Language::MaxCount)
ret = GameDatabase::GetLanguageName(custom_language);
else if (dbentry)
ret = dbentry->GetLanguageFlagName(region);
else
{
ret = Settings::GetDiscRegionName(region);
}

return ret;
}

TinyString GameList::Entry::GetLanguageIconFileName() const
TinyString GameList::Entry::GetLanguageIconName() const
{
return TinyString::from_format("images/flags/{}.svg", GetLanguageIcon());
return GameDatabase::GetLanguageFlagResourceName(GetLanguageIcon());
}

TinyString GameList::Entry::GetCompatibilityIconFileName() const
Expand Down Expand Up @@ -1518,28 +1529,37 @@ std::string GameList::GetCustomPropertiesFile()
return Path::Combine(EmuFolders::DataRoot, "custom_properties.ini");
}

void GameList::SaveCustomTitleForPath(const std::string& path, const std::string& custom_title)
bool GameList::PutCustomPropertiesField(INISettingsInterface& ini, const std::string& path, const char* field,
const char* value)
{
INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile());
custom_attributes_ini.Load();
ini.Load();

if (!custom_title.empty())
if (value && *value != '\0')
{
custom_attributes_ini.SetStringValue(path.c_str(), "Title", custom_title.c_str());
ini.SetStringValue(path.c_str(), field, value);
}
else
{
custom_attributes_ini.DeleteValue(path.c_str(), "Title");
custom_attributes_ini.RemoveEmptySections();
ini.DeleteValue(path.c_str(), field);
ini.RemoveEmptySections();
}

Error error;
if (!custom_attributes_ini.Save(&error))
if (!ini.Save(&error))
{
ERROR_LOG("Failed to save custom attributes: {}", error.GetDescription());
return;
return false;
}

return true;
}

bool GameList::SaveCustomTitleForPath(const std::string& path, const std::string& custom_title)
{
INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile());
if (!PutCustomPropertiesField(custom_attributes_ini, path, "Title", custom_title.c_str()))
return false;

if (!custom_title.empty())
{
// Can skip the rescan and just update the value directly.
Expand All @@ -1556,28 +1576,18 @@ void GameList::SaveCustomTitleForPath(const std::string& path, const std::string
// Let the cache update by rescanning. Only need to do this on deletion, to get the original value.
RescanCustomAttributesForPath(path, custom_attributes_ini);
}

return true;
}

void GameList::SaveCustomRegionForPath(const std::string& path, const std::optional<DiscRegion> custom_region)
bool 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())
if (!PutCustomPropertiesField(custom_attributes_ini, path, "Region",
custom_region.has_value() ? Settings::GetDiscRegionName(custom_region.value()) :
nullptr))
{
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;
return false;
}

if (custom_region.has_value())
Expand All @@ -1596,6 +1606,28 @@ void GameList::SaveCustomRegionForPath(const std::string& path, const std::optio
// Let the cache update by rescanning. Only need to do this on deletion, to get the original value.
RescanCustomAttributesForPath(path, custom_attributes_ini);
}

return true;
}

bool GameList::SaveCustomLanguageForPath(const std::string& path,
const std::optional<GameDatabase::Language> custom_language)
{
INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile());
if (!PutCustomPropertiesField(custom_attributes_ini, path, "Language",
custom_language.has_value() ? GameDatabase::GetLanguageName(custom_language.value()) :
nullptr))
{
return false;
}

// Don't need to rescan, since there's no original value to restore.
auto lock = GetLock();
Entry* entry = GetMutableEntryForPath(path);
if (entry)
entry->custom_language = custom_language.value_or(GameDatabase::Language::MaxCount);

return true;
}

std::string GameList::GetCustomTitleForPath(const std::string_view path)
Expand Down
9 changes: 6 additions & 3 deletions src/core/game_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct Entry
bool disc_set_member = false;
bool has_custom_title = false;
bool has_custom_region = false;
GameDatabase::Language custom_language = GameDatabase::Language::MaxCount;

std::string path;
std::string serial;
Expand All @@ -57,13 +58,14 @@ struct Entry

std::string_view GetLanguageIcon() const;

TinyString GetLanguageIconFileName() const;
TinyString GetLanguageIconName() const;
TinyString GetCompatibilityIconFileName() const;

TinyString GetReleaseDateString() const;

ALWAYS_INLINE bool IsDisc() const { return (type == EntryType::Disc); }
ALWAYS_INLINE bool IsDiscSet() const { return (type == EntryType::DiscSet); }
ALWAYS_INLINE bool HasCustomLanguage() const { return (custom_language != GameDatabase::Language::MaxCount); }
ALWAYS_INLINE EntryType GetSortType() const { return (type == EntryType::DiscSet) ? EntryType::Disc : type; }
};

Expand Down Expand Up @@ -128,8 +130,9 @@ bool DownloadCovers(const std::vector<std::string>& url_templates, bool use_seri
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);
bool SaveCustomTitleForPath(const std::string& path, const std::string& custom_title);
bool SaveCustomRegionForPath(const std::string& path, const std::optional<DiscRegion> custom_region);
bool SaveCustomLanguageForPath(const std::string& path, const std::optional<GameDatabase::Language> custom_language);
std::string GetCustomTitleForPath(const std::string_view path);
std::optional<DiscRegion> GetCustomRegionForPath(const std::string_view path);

Expand Down
2 changes: 1 addition & 1 deletion src/duckstation-qt/gamelistmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ const QPixmap& GameListModel::getFlagPixmapForEntry(const GameList::Entry* ge) c
if (it != m_flag_pixmap_cache.end())
return it->second;

const QIcon icon(QString::fromStdString(QtHost::GetResourcePath(ge->GetLanguageIconFileName(), true)));
const QIcon icon(QString::fromStdString(QtHost::GetResourcePath(ge->GetLanguageIconName(), true)));
it = m_flag_pixmap_cache.emplace(name, icon.pixmap(FLAG_PIXMAP_WIDTH, FLAG_PIXMAP_HEIGHT)).first;
return it->second;
}
Expand Down
31 changes: 30 additions & 1 deletion src/duckstation-qt/gamesummarywidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ GameSummaryWidget::GameSummaryWidget(const std::string& path, const std::string&
static_cast<GameDatabase::CompatibilityRating>(i))));
}

// I hate this so much.
m_ui.customLanguage->addItem(QtUtils::GetIconForLanguage(entry->GetLanguageFlagName(region)),
tr("Show Default Flag"));
for (u32 i = 0; i < static_cast<u32>(GameDatabase::Language::MaxCount); i++)
{
const char* language_name = GameDatabase::GetLanguageName(static_cast<GameDatabase::Language>(i));
m_ui.customLanguage->addItem(QtUtils::GetIconForLanguage(language_name), QString::fromUtf8(language_name));
}

populateUi(path, serial, region, entry);

connect(m_ui.compatibilityComments, &QToolButton::clicked, this, &GameSummaryWidget::onCompatibilityCommentsClicked);
Expand All @@ -69,6 +78,7 @@ GameSummaryWidget::GameSummaryWidget(const std::string& path, const std::string&
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); });
connect(m_ui.customLanguage, &QComboBox::currentIndexChanged, this, &GameSummaryWidget::onCustomLanguageChanged);
}

GameSummaryWidget::~GameSummaryWidget() = default;
Expand Down Expand Up @@ -147,6 +157,8 @@ void GameSummaryWidget::populateUi(const std::string& path, const std::string& s
else
m_ui.releaseInfo->setText(tr("Unknown"));

m_ui.languages->setText(QtUtils::StringViewToQString(entry->GetLanguagesString()));

QString controllers;
if (entry->supported_controllers != 0 && entry->supported_controllers != static_cast<u16>(-1))
{
Expand Down Expand Up @@ -201,7 +213,10 @@ void GameSummaryWidget::populateCustomAttributes()
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(m_path);
if (!entry || entry->IsDiscSet())
{
m_ui.customLanguage->setEnabled(false);
return;
}

{
QSignalBlocker sb(m_ui.title);
Expand All @@ -214,6 +229,12 @@ void GameSummaryWidget::populateCustomAttributes()
m_ui.region->setCurrentIndex(static_cast<int>(entry->region));
m_ui.restoreRegion->setEnabled(entry->has_custom_region);
}

{
QSignalBlocker sb(m_ui.customLanguage);
m_ui.customLanguage->setCurrentIndex(entry->HasCustomLanguage() ? (static_cast<u32>(entry->custom_language) + 1) :
0);
}
}

void GameSummaryWidget::updateWindowTitle()
Expand All @@ -238,7 +259,15 @@ void GameSummaryWidget::setCustomRegion(int region)
GameList::SaveCustomRegionForPath(m_path, (region >= 0) ? std::optional<DiscRegion>(static_cast<DiscRegion>(region)) :
std::optional<DiscRegion>());
populateCustomAttributes();
updateWindowTitle();
g_main_window->refreshGameListModel();
}

void GameSummaryWidget::onCustomLanguageChanged(int language)
{
GameList::SaveCustomLanguageForPath(
m_path, (language > 0) ? std::optional<GameDatabase::Language>(static_cast<GameDatabase::Language>(language - 1)) :
std::optional<GameDatabase::Language>());
populateCustomAttributes();
g_main_window->refreshGameListModel();
}

Expand Down
1 change: 1 addition & 0 deletions src/duckstation-qt/gamesummarywidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class GameSummaryWidget : public QWidget
void reloadGameSettings();

private Q_SLOTS:
void onCustomLanguageChanged(int language);
void onCompatibilityCommentsClicked();
void onInputProfileChanged(int index);
void onEditInputProfileClicked();
Expand Down
Loading

0 comments on commit 852239e

Please sign in to comment.