From 325dcc81ca2aa2bbb8a14fbd69294b486a2b37e5 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 29 Nov 2023 20:26:36 +1000 Subject: [PATCH] FullscreenUI: Allow changing UI language --- scripts/generate_update_fa_glyph_ranges.py | 2 +- src/core/fullscreen_ui.cpp | 30 ++++++++++++++ src/core/host.h | 6 +++ src/duckstation-nogui/nogui_host.cpp | 10 +++++ src/duckstation-qt/mainwindow.cpp | 23 +++++------ src/duckstation-qt/mainwindow.h | 2 +- src/duckstation-qt/qthost.h | 3 -- src/duckstation-qt/qttranslations.cpp | 48 ++++++++++++++-------- src/duckstation-qt/setupwizarddialog.cpp | 4 +- src/duckstation-regtest/regtest_host.cpp | 10 +++++ src/util/imgui_fullscreen.cpp | 4 +- src/util/imgui_manager.cpp | 16 ++++---- 12 files changed, 111 insertions(+), 47 deletions(-) diff --git a/scripts/generate_update_fa_glyph_ranges.py b/scripts/generate_update_fa_glyph_ranges.py index 571dc688b..f3add874d 100644 --- a/scripts/generate_update_fa_glyph_ranges.py +++ b/scripts/generate_update_fa_glyph_ranges.py @@ -81,7 +81,7 @@ def get_pairs(tokens): with open(dst_file, "r") as f: original = f.read() updated = re.sub(out_pattern, "\\1 " + get_pairs(tokens) + " \\2", original) - updated = re.sub(out_pf_pattern, "\\1 " + get_pairs(pf_tokens) + " \\2", original) + updated = re.sub(out_pf_pattern, "\\1 " + get_pairs(pf_tokens) + " \\2", updated) if original != updated: with open(dst_file, "w") as f: f.write(updated) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 188f40881..f7734324f 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -2795,6 +2795,34 @@ void FullscreenUI::DrawInterfaceSettingsPage() ImGuiFullscreen::SetTheme(bsi->GetBoolValue("Main", "UseLightFullscreenUITheme", false)); } + { + // Have to do this the annoying way, because it's host-derived. + const auto language_list = Host::GetAvailableLanguageList(); + std::string current_language = bsi->GetStringValue("Main", "Language", ""); + const char* current_language_name = "Unknown"; + for (const auto& [language, code] : language_list) + { + if (current_language == code) + current_language_name = language; + } + if (MenuButtonWithValue(FSUI_ICONSTR(ICON_FA_LANGUAGE, "UI Language"), + FSUI_CSTR("Chooses the language used for UI elements."), current_language_name)) + { + ImGuiFullscreen::ChoiceDialogOptions options; + for (const auto& [language, code] : language_list) + options.emplace_back(fmt::format("{} [{}]", language, code), (current_language == code)); + OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_LANGUAGE, "UI Language"), false, std::move(options), + [language_list](s32 index, const std::string& title, bool checked) { + if (static_cast(index) >= language_list.size()) + return; + + ImGuiFullscreen::CloseChoiceDialog(); + Host::RunOnCPUThread( + [language = language_list[index].second]() { Host::ChangeLanguage(language); }); + }); + } + } + #ifdef ENABLE_DISCORD_PRESENCE MenuHeading(FSUI_CSTR("Integration")); DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_CHARGING_STATION, "Enable Discord Presence"), @@ -6777,6 +6805,7 @@ TRANSLATE_NOOP("FullscreenUI", "Change settings for the emulator."); TRANSLATE_NOOP("FullscreenUI", "Changes the aspect ratio used to display the console's output to the screen."); TRANSLATE_NOOP("FullscreenUI", "Cheat List"); TRANSLATE_NOOP("FullscreenUI", "Chooses the backend to use for rendering the console/game visuals."); +TRANSLATE_NOOP("FullscreenUI", "Chooses the language used for UI elements."); TRANSLATE_NOOP("FullscreenUI", "Chroma Smoothing For 24-Bit Display"); TRANSLATE_NOOP("FullscreenUI", "Clean Boot"); TRANSLATE_NOOP("FullscreenUI", "Clear Settings"); @@ -7218,6 +7247,7 @@ TRANSLATE_NOOP("FullscreenUI", "Toggle every %d frames"); TRANSLATE_NOOP("FullscreenUI", "True Color Rendering"); TRANSLATE_NOOP("FullscreenUI", "Turbo Speed"); TRANSLATE_NOOP("FullscreenUI", "Type"); +TRANSLATE_NOOP("FullscreenUI", "UI Language"); TRANSLATE_NOOP("FullscreenUI", "Undo Load State"); TRANSLATE_NOOP("FullscreenUI", "Unknown"); TRANSLATE_NOOP("FullscreenUI", "Unlimited"); diff --git a/src/core/host.h b/src/core/host.h index ccde4c78d..177a81bdd 100644 --- a/src/core/host.h +++ b/src/core/host.h @@ -75,6 +75,12 @@ std::unique_ptr CreateAudioStream(AudioBackend backend, u32 sample_ void ReportDebuggerMessage(const std::string_view& message); void ReportFormattedDebuggerMessage(const char* format, ...); +/// Returns a list of supported languages and codes (suffixes for translation files). +std::span> GetAvailableLanguageList(); + +/// Refreshes the UI when the language is changed. +bool ChangeLanguage(const char* new_language); + /// Displays a loading screen with the logo, rendered with ImGui. Use when executing possibly-time-consuming tasks /// such as compiling shaders when starting up. void DisplayLoadingScreen(const char* message, int progress_min = -1, int progress_max = -1, int progress_value = -1); diff --git a/src/duckstation-nogui/nogui_host.cpp b/src/duckstation-nogui/nogui_host.cpp index 777dd8a3f..5543c2c99 100644 --- a/src/duckstation-nogui/nogui_host.cpp +++ b/src/duckstation-nogui/nogui_host.cpp @@ -322,6 +322,16 @@ void Host::ReportDebuggerMessage(const std::string_view& message) Log_ErrorPrintf("ReportDebuggerMessage: %.*s", static_cast(message.size()), message.data()); } +std::span> Host::GetAvailableLanguageList() +{ + return {}; +} + +bool Host::ChangeLanguage(const char* new_language) +{ + return false; +} + void Host::AddFixedInputBindings(SettingsInterface& si) { } diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 39456bd77..716b38082 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -1648,32 +1648,29 @@ void MainWindow::setupAdditionalUi() } updateDebugMenuCropMode(); - const QString current_language(QString::fromStdString(Host::GetBaseStringSettingValue("Main", "Language", ""))); + const std::string current_language = Host::GetBaseStringSettingValue("Main", "Language", ""); QActionGroup* language_group = new QActionGroup(m_ui.menuSettingsLanguage); - for (const std::pair& it : QtHost::GetAvailableLanguageList()) + for (const auto& [language, code] : Host::GetAvailableLanguageList()) { - QAction* action = language_group->addAction(it.first); + QAction* action = language_group->addAction(QString::fromUtf8(language)); action->setCheckable(true); - action->setChecked(current_language == it.second); + action->setChecked(current_language == code); - QString icon_filename(QStringLiteral(":/icons/flags/%1.png").arg(it.second)); + QString icon_filename(QStringLiteral(":/icons/flags/%1.png").arg(QLatin1StringView(code))); if (!QFile::exists(icon_filename)) { // try without the suffix (e.g. es-es -> es) - const int pos = it.second.lastIndexOf('-'); - if (pos >= 0) - icon_filename = QStringLiteral(":/icons/flags/%1.png").arg(it.second.left(pos)); + const char* pos = std::strrchr(code, '-'); + if (pos) + icon_filename = QStringLiteral(":/icons/flags/%1.png").arg(QLatin1StringView(pos)); } action->setIcon(QIcon(icon_filename)); m_ui.menuSettingsLanguage->addAction(action); - action->setData(it.second); + action->setData(QString::fromLatin1(code)); connect(action, &QAction::triggered, [this, action]() { const QString new_language = action->data().toString(); - Host::SetBaseStringSettingValue("Main", "Language", new_language.toUtf8().constData()); - Host::CommitBaseSettingChanges(); - QtHost::InstallTranslator(); - recreate(); + Host::ChangeLanguage(new_language.toUtf8().constData()); }); } diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 4f165f741..760e9c708 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -107,6 +107,7 @@ public Q_SLOTS: std::optional getWindowInfo(); void checkForUpdates(bool display_message); + void recreate(); void* getNativeWindowId(); @@ -242,7 +243,6 @@ private: void setTheme(const QString& theme); void updateTheme(); void reloadThemeSpecificImages(); - void recreate(); void destroySubWindows(); void registerForDeviceNotifications(); diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index a270bbee2..35e1f8685 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -253,9 +253,6 @@ bool InNoGUIMode(); /// Executes a function on the UI thread. void RunOnUIThread(const std::function& func, bool block = false); -/// Returns a list of supported languages and codes (suffixes for translation files). -std::vector> GetAvailableLanguageList(); - /// Default language for the platform. const char* GetDefaultLanguage(); diff --git a/src/duckstation-qt/qttranslations.cpp b/src/duckstation-qt/qttranslations.cpp index 99dc9a127..84719543e 100644 --- a/src/duckstation-qt/qttranslations.cpp +++ b/src/duckstation-qt/qttranslations.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin and contributors. // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) +#include "mainwindow.h" #include "qthost.h" #include "core/host.h" @@ -202,24 +203,37 @@ static std::string QtHost::GetFontPath(const GlyphInfo* gi) return font_path; } -std::vector> QtHost::GetAvailableLanguageList() +std::span> Host::GetAvailableLanguageList() { - return {{QStringLiteral("English"), QStringLiteral("en")}, - {QStringLiteral("Deutsch"), QStringLiteral("de")}, - {QStringLiteral("Español de Latinoamérica"), QStringLiteral("es")}, - {QStringLiteral("Español de España"), QStringLiteral("es-ES")}, - {QStringLiteral("Français"), QStringLiteral("fr")}, - {QStringLiteral("עברית"), QStringLiteral("he")}, - {QStringLiteral("日本語"), QStringLiteral("ja")}, - {QStringLiteral("한국어"), QStringLiteral("ko")}, - {QStringLiteral("Italiano"), QStringLiteral("it")}, - {QStringLiteral("Nederlands"), QStringLiteral("nl")}, - {QStringLiteral("Polski"), QStringLiteral("pl")}, - {QStringLiteral("Português (Pt)"), QStringLiteral("pt-PT")}, - {QStringLiteral("Português (Br)"), QStringLiteral("pt-BR")}, - {QStringLiteral("Русский"), QStringLiteral("ru")}, - {QStringLiteral("Türkçe"), QStringLiteral("tr")}, - {QStringLiteral("简体中文"), QStringLiteral("zh-CN")}}; + static constexpr const std::pair languages[] = {{"English", "en"}, + {"Deutsch", "de"}, + {"Español de Latinoamérica", "es"}, + {"Español de España", "es-ES"}, + {"Français", "fr"}, + {"עברית", "he"}, + {"日本語", "ja"}, + {"한국어", "ko"}, + {"Italiano", "it"}, + {"Nederlands", "nl"}, + {"Polski", "pl"}, + {"Português (Pt)", "pt-PT"}, + {"Português (Br)", "pt-BR"}, + {"Русский", "ru"}, + {"Türkçe", "tr"}, + {"简体中文", "zh-CN"}}; + + return languages; +} + +bool Host::ChangeLanguage(const char* new_language) +{ + QtHost::RunOnUIThread([new_language = std::string(new_language)]() { + Host::SetBaseStringSettingValue("Main", "Language", new_language.c_str()); + Host::CommitBaseSettingChanges(); + QtHost::InstallTranslator(); + g_main_window->recreate(); + }); + return true; } const char* QtHost::GetDefaultLanguage() diff --git a/src/duckstation-qt/setupwizarddialog.cpp b/src/duckstation-qt/setupwizarddialog.cpp index 11eccdaf5..1efc836b9 100644 --- a/src/duckstation-qt/setupwizarddialog.cpp +++ b/src/duckstation-qt/setupwizarddialog.cpp @@ -186,8 +186,8 @@ void SetupWizardDialog::setupLanguagePage() GeneralSettingsWidget::DEFAULT_THEME_NAME, "InterfaceSettingsWidget"); connect(m_ui.theme, QOverload::of(&QComboBox::currentIndexChanged), this, &SetupWizardDialog::themeChanged); - for (const std::pair& it : QtHost::GetAvailableLanguageList()) - m_ui.language->addItem(it.first, it.second); + for (const auto& [language, code] : Host::GetAvailableLanguageList()) + m_ui.language->addItem(QString::fromUtf8(language), QString::fromLatin1(code)); SettingWidgetBinder::BindWidgetToStringSetting(nullptr, m_ui.language, "Main", "Language", QtHost::GetDefaultLanguage()); connect(m_ui.language, QOverload::of(&QComboBox::currentIndexChanged), this, diff --git a/src/duckstation-regtest/regtest_host.cpp b/src/duckstation-regtest/regtest_host.cpp index eac8aec7a..69bdcdbc3 100644 --- a/src/duckstation-regtest/regtest_host.cpp +++ b/src/duckstation-regtest/regtest_host.cpp @@ -146,6 +146,16 @@ void Host::ReportDebuggerMessage(const std::string_view& message) Log_ErrorPrintf("ReportDebuggerMessage: %.*s", static_cast(message.size()), message.data()); } +std::span> Host::GetAvailableLanguageList() +{ + return {}; +} + +bool Host::ChangeLanguage(const char* new_language) +{ + return false; +} + s32 Host::Internal::GetTranslatedStringImpl(const std::string_view& context, const std::string_view& msg, char* tbuf, size_t tbuf_space) { diff --git a/src/util/imgui_fullscreen.cpp b/src/util/imgui_fullscreen.cpp index 04ce40416..fb1b5b1b3 100644 --- a/src/util/imgui_fullscreen.cpp +++ b/src/util/imgui_fullscreen.cpp @@ -404,14 +404,14 @@ bool ImGuiFullscreen::UpdateLayoutScale() if (screen_ratio > LAYOUT_RATIO) { // screen is wider, use height, pad width - g_layout_scale = screen_height / LAYOUT_SCREEN_HEIGHT; + g_layout_scale = std::max(screen_height / LAYOUT_SCREEN_HEIGHT, 1.0f); g_layout_padding_top = 0.0f; g_layout_padding_left = (screen_width - (LAYOUT_SCREEN_WIDTH * g_layout_scale)) / 2.0f; } else { // screen is taller, use width, pad height - g_layout_scale = screen_width / LAYOUT_SCREEN_WIDTH; + g_layout_scale = std::max(screen_width / LAYOUT_SCREEN_WIDTH, 1.0f); g_layout_padding_top = (screen_height - (LAYOUT_SCREEN_HEIGHT * g_layout_scale)) / 2.0f; g_layout_padding_left = 0.0f; } diff --git a/src/util/imgui_manager.cpp b/src/util/imgui_manager.cpp index 25056f788..d5f8721b4 100644 --- a/src/util/imgui_manager.cpp +++ b/src/util/imgui_manager.cpp @@ -251,7 +251,7 @@ void ImGuiManager::UpdateScale() const float window_scale = g_gpu_device ? g_gpu_device->GetWindowScale() : 1.0f; const float scale = std::max(window_scale * s_global_prescale, 1.0f); - if (scale == s_global_scale && (!HasFullscreenFonts() || !ImGuiFullscreen::UpdateLayoutScale())) + if ((!HasFullscreenFonts() || !ImGuiFullscreen::UpdateLayoutScale()) && scale == s_global_scale) return; s_global_scale = scale; @@ -556,18 +556,18 @@ bool ImGuiManager::AddIconFonts(float size) { static constexpr ImWchar range_fa[] = { 0xf002, 0xf002, 0xf005, 0xf005, 0xf007, 0xf007, 0xf00c, 0xf00e, 0xf011, 0xf011, 0xf013, 0xf013, 0xf017, 0xf017, - 0xf019, 0xf019, 0xf01c, 0xf01c, 0xf021, 0xf021, 0xf023, 0xf023, 0xf025, 0xf025, 0xf027, 0xf028, 0xf02d, 0xf02e, + 0xf019, 0xf019, 0xf01c, 0xf01c, 0xf021, 0xf021, 0xf023, 0xf023, 0xf025, 0xf025, 0xf027, 0xf028, 0xf02e, 0xf02e, 0xf030, 0xf030, 0xf03a, 0xf03a, 0xf03d, 0xf03d, 0xf049, 0xf04c, 0xf050, 0xf050, 0xf059, 0xf059, 0xf05e, 0xf05e, 0xf062, 0xf063, 0xf065, 0xf065, 0xf067, 0xf067, 0xf071, 0xf071, 0xf075, 0xf075, 0xf077, 0xf078, 0xf07b, 0xf07c, 0xf084, 0xf085, 0xf091, 0xf091, 0xf0a0, 0xf0a0, 0xf0ac, 0xf0ad, 0xf0c5, 0xf0c5, 0xf0c7, 0xf0c8, 0xf0cb, 0xf0cb, 0xf0d0, 0xf0d0, 0xf0dc, 0xf0dc, 0xf0e2, 0xf0e2, 0xf0eb, 0xf0eb, 0xf0f1, 0xf0f1, 0xf0f3, 0xf0f3, 0xf0fe, 0xf0fe, 0xf110, 0xf110, 0xf119, 0xf119, 0xf11b, 0xf11c, 0xf140, 0xf140, 0xf144, 0xf144, 0xf14a, 0xf14a, 0xf15b, 0xf15b, - 0xf15d, 0xf15d, 0xf188, 0xf188, 0xf191, 0xf192, 0xf1dd, 0xf1de, 0xf1e6, 0xf1e6, 0xf1eb, 0xf1eb, 0xf1f8, 0xf1f8, - 0xf1fc, 0xf1fc, 0xf242, 0xf242, 0xf245, 0xf245, 0xf26c, 0xf26c, 0xf279, 0xf279, 0xf2d0, 0xf2d0, 0xf2db, 0xf2db, - 0xf2f2, 0xf2f2, 0xf2f5, 0xf2f5, 0xf3c1, 0xf3c1, 0xf410, 0xf410, 0xf466, 0xf466, 0xf500, 0xf500, 0xf51f, 0xf51f, - 0xf545, 0xf545, 0xf547, 0xf548, 0xf552, 0xf552, 0xf57a, 0xf57a, 0xf5a2, 0xf5a2, 0xf5aa, 0xf5aa, 0xf5e7, 0xf5e7, - 0xf65d, 0xf65e, 0xf6a9, 0xf6a9, 0xf7c2, 0xf7c2, 0xf807, 0xf807, 0xf815, 0xf815, 0xf818, 0xf818, 0xf84c, 0xf84c, - 0xf8cc, 0xf8cc, 0x0, 0x0}; + 0xf15d, 0xf15d, 0xf188, 0xf188, 0xf191, 0xf192, 0xf1ab, 0xf1ab, 0xf1dd, 0xf1de, 0xf1e6, 0xf1e6, 0xf1eb, 0xf1eb, + 0xf1f8, 0xf1f8, 0xf1fc, 0xf1fc, 0xf242, 0xf242, 0xf245, 0xf245, 0xf26c, 0xf26c, 0xf279, 0xf279, 0xf2d0, 0xf2d0, + 0xf2db, 0xf2db, 0xf2f2, 0xf2f2, 0xf2f5, 0xf2f5, 0xf3c1, 0xf3c1, 0xf410, 0xf410, 0xf466, 0xf466, 0xf500, 0xf500, + 0xf51f, 0xf51f, 0xf545, 0xf545, 0xf547, 0xf548, 0xf552, 0xf552, 0xf57a, 0xf57a, 0xf5a2, 0xf5a2, 0xf5aa, 0xf5aa, + 0xf5e7, 0xf5e7, 0xf65d, 0xf65e, 0xf6a9, 0xf6a9, 0xf7c2, 0xf7c2, 0xf807, 0xf807, 0xf815, 0xf815, 0xf818, 0xf818, + 0xf84c, 0xf84c, 0xf8cc, 0xf8cc, 0x0, 0x0}; static constexpr ImWchar range_pf[] = {0x2196, 0x2199, 0x219e, 0x21a1, 0x21b0, 0x21b3, 0x21ba, 0x21c3, 0x21c7, 0x21ca, 0x21d0, 0x21d4, 0x21dc, 0x21dd, 0x21e0, 0x21e3, 0x21ed, 0x21ee, 0x21f7, 0x21f8, 0x21fa, 0x21fb, 0x227a, 0x227d, 0x23f4, 0x23f7, 0x2427, 0x243a, 0x243c, 0x243c,