mirror of https://github.com/PCSX2/pcsx2.git
385 lines
13 KiB
C++
385 lines
13 KiB
C++
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "MainWindow.h"
|
|
#include "QtHost.h"
|
|
|
|
#include "common/Assertions.h"
|
|
#include "common/Console.h"
|
|
#include "common/FileSystem.h"
|
|
#include "common/Path.h"
|
|
#include "common/SmallString.h"
|
|
#include "common/StringUtil.h"
|
|
|
|
#include "pcsx2/ImGui/FullscreenUI.h"
|
|
#include "pcsx2/ImGui/ImGuiManager.h"
|
|
#include "pcsx2/MTGS.h"
|
|
|
|
#include "fmt/format.h"
|
|
#include "imgui.h"
|
|
|
|
#include <QtCore/QCollator>
|
|
#include <QtCore/QFile>
|
|
#include <QtCore/QTranslator>
|
|
#include <QtGui/QGuiApplication>
|
|
#include <QtWidgets/QMessageBox>
|
|
|
|
#include <optional>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
#include "common/RedtapeWindows.h"
|
|
#include <KnownFolders.h>
|
|
#include <ShlObj.h>
|
|
#endif
|
|
|
|
#if 0
|
|
// Qt internal strings we'd like to have translated
|
|
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Services")
|
|
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Hide %1")
|
|
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Hide Others")
|
|
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Show All")
|
|
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Preferences...")
|
|
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Quit %1")
|
|
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "About %1")
|
|
|
|
// Strings that will be parsed out by our build system and sent to fun places
|
|
QT_TRANSLATE_NOOP("PermissionsDialogMicrophone", "PCSX2 uses your microphone to emulate a USB microphone plugged into the virtual PS2.")
|
|
QT_TRANSLATE_NOOP("PermissionsDialogCamera", "PCSX2 uses your camera to emulate an EyeToy camera plugged into the virtual PS2.")
|
|
#endif
|
|
|
|
namespace QtHost
|
|
{
|
|
struct FontInfo
|
|
{
|
|
const char* language;
|
|
const char* imgui_font_name;
|
|
};
|
|
|
|
static void UpdateGlyphRangesAndClearCache(QWidget* dialog_parent, const std::string_view language);
|
|
static bool DownloadMissingFont(QWidget* dialog_parent, const char* font_name, const std::string& path);
|
|
static const FontInfo* GetFontInfo(const std::string_view language);
|
|
|
|
static constexpr const char* DEFAULT_IMGUI_FONT_NAME = "Roboto-Regular.ttf";
|
|
|
|
static QLocale s_current_locale;
|
|
static QCollator s_current_collator;
|
|
|
|
static std::vector<QTranslator*> s_translators;
|
|
} // namespace QtHost
|
|
|
|
static QString getSystemLanguage()
|
|
{
|
|
std::vector<std::pair<QString, QString>> available = QtHost::GetAvailableLanguageList();
|
|
QString locale = QLocale::system().name();
|
|
locale.replace('_', '-');
|
|
// Can we find an exact match?
|
|
for (const std::pair<QString, QString>& entry : available)
|
|
{
|
|
if (entry.second == locale)
|
|
return locale;
|
|
}
|
|
// How about a partial match?
|
|
QStringView lang = QStringView(locale);
|
|
lang = lang.left(lang.indexOf('-'));
|
|
for (const std::pair<QString, QString>& entry : available)
|
|
{
|
|
QStringView avail = QStringView(entry.second);
|
|
avail = avail.left(avail.indexOf('-'));
|
|
if (avail == lang)
|
|
{
|
|
Console.Warning("Couldn't find translation for system language %s, using %s instead",
|
|
locale.toStdString().c_str(), entry.second.toStdString().c_str());
|
|
return entry.second;
|
|
}
|
|
}
|
|
// No matches :(
|
|
Console.Warning("Couldn't find translation for system language %s, using en instead", locale.toStdString().c_str());
|
|
return QStringLiteral("en-US");
|
|
}
|
|
|
|
void QtHost::InstallTranslator(QWidget* dialog_parent)
|
|
{
|
|
for (QTranslator* translator : s_translators)
|
|
{
|
|
qApp->removeTranslator(translator);
|
|
translator->deleteLater();
|
|
}
|
|
s_translators.clear();
|
|
|
|
QString language =
|
|
QString::fromStdString(Host::GetBaseStringSettingValue("UI", "Language", GetDefaultLanguage()));
|
|
if (language == QStringLiteral("system"))
|
|
language = getSystemLanguage();
|
|
|
|
QString qlanguage = language;
|
|
qlanguage.replace('-', '_');
|
|
s_current_locale = QLocale(qlanguage);
|
|
s_current_collator = QCollator(s_current_locale);
|
|
|
|
// Install the base qt translation first.
|
|
#if defined(__APPLE__)
|
|
const QString base_dir = QStringLiteral("%1/../Resources/translations").arg(qApp->applicationDirPath());
|
|
#elif defined(PCSX2_APP_DATADIR)
|
|
const QString base_dir = QStringLiteral("%1/%2/translations").arg(qApp->applicationDirPath()).arg(PCSX2_APP_DATADIR);
|
|
#else
|
|
const QString base_dir = QStringLiteral("%1/translations").arg(qApp->applicationDirPath());
|
|
#endif
|
|
|
|
// Qt base uses underscores instead of hyphens.
|
|
const QString qt_language = QString(language).replace(QChar('-'), QChar('_'));
|
|
QString base_path = QStringLiteral("%1/qt_%2.qm").arg(base_dir).arg(qt_language);
|
|
bool has_base_ts = QFile::exists(base_path);
|
|
if (!has_base_ts)
|
|
{
|
|
// Try without the country suffix.
|
|
const int index = language.lastIndexOf('-');
|
|
if (index > 0)
|
|
{
|
|
base_path = QStringLiteral("%1/qt_%2.qm").arg(base_dir).arg(language.left(index));
|
|
has_base_ts = QFile::exists(base_path);
|
|
}
|
|
}
|
|
|
|
if (has_base_ts)
|
|
{
|
|
QTranslator* base_translator = new QTranslator(qApp);
|
|
if (!base_translator->load(base_path))
|
|
{
|
|
QMessageBox::warning(nullptr, QStringLiteral("Translation Error"),
|
|
QStringLiteral("Failed to find load base translation file for '%1':\n%2").arg(language).arg(base_path));
|
|
delete base_translator;
|
|
}
|
|
else
|
|
{
|
|
s_translators.push_back(base_translator);
|
|
qApp->installTranslator(base_translator);
|
|
}
|
|
}
|
|
|
|
const QString path = QStringLiteral("%1/pcsx2-qt_%3.qm").arg(base_dir).arg(language);
|
|
QTranslator* translator = nullptr;
|
|
if (QFile::exists(path))
|
|
{
|
|
translator = new QTranslator(qApp);
|
|
if (translator->load(path))
|
|
{
|
|
Console.WriteLn(
|
|
Color_StrongYellow, "Loaded translation file for language %s", language.toUtf8().constData());
|
|
}
|
|
else
|
|
{
|
|
QMessageBox::warning(nullptr, QStringLiteral("Translation Error"),
|
|
QStringLiteral("Failed to load translation file for language '%1':\n%2").arg(language).arg(path));
|
|
delete translator;
|
|
translator = nullptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
#ifdef PCSX2_DEVBUILD
|
|
// For now, until we're sure this works on all platforms, we won't block users from starting if they're missing.
|
|
QMessageBox::warning(nullptr, QStringLiteral("Translation Error"),
|
|
QStringLiteral("Failed to find translation file for language '%1':\n%2").arg(language).arg(path));
|
|
#endif
|
|
}
|
|
|
|
if (translator)
|
|
{
|
|
qApp->installTranslator(translator);
|
|
s_translators.push_back(translator);
|
|
}
|
|
|
|
UpdateGlyphRangesAndClearCache(dialog_parent, language.toStdString());
|
|
|
|
if (FullscreenUI::IsInitialized())
|
|
{
|
|
MTGS::RunOnGSThread([]() mutable {
|
|
FullscreenUI::LocaleChanged();
|
|
});
|
|
}
|
|
}
|
|
|
|
const char* QtHost::GetDefaultLanguage()
|
|
{
|
|
return "system";
|
|
}
|
|
|
|
s32 Host::Internal::GetTranslatedStringImpl(
|
|
const std::string_view context, const std::string_view msg, char* tbuf, size_t tbuf_space)
|
|
{
|
|
// This is really awful. Thankfully we're caching the results...
|
|
const std::string temp_context(context);
|
|
const std::string temp_msg(msg);
|
|
const QString translated_msg = qApp->translate(temp_context.c_str(), temp_msg.c_str());
|
|
const QByteArray translated_utf8 = translated_msg.toUtf8();
|
|
const size_t translated_size = translated_utf8.size();
|
|
if (translated_size > tbuf_space)
|
|
return -1;
|
|
else if (translated_size > 0)
|
|
std::memcpy(tbuf, translated_utf8.constData(), translated_size);
|
|
|
|
return static_cast<s32>(translated_size);
|
|
}
|
|
|
|
std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)
|
|
{
|
|
return qApp->translate(context, msg, disambiguation, count).toStdString();
|
|
}
|
|
|
|
bool Host::LocaleCircleConfirm()
|
|
{
|
|
QLocale& loc = QtHost::s_current_locale;
|
|
return (loc.language() == QLocale::Japanese) || (loc.language() == QLocale::Chinese) || (loc.language() == QLocale::Korean);
|
|
}
|
|
|
|
std::vector<std::pair<QString, QString>> QtHost::GetAvailableLanguageList()
|
|
{
|
|
return {
|
|
{QCoreApplication::translate("InterfaceSettingsWidget", "System Language [Default]"), QStringLiteral("system")},
|
|
{QStringLiteral("Afrikaans (af-ZA)"), QStringLiteral("af-ZA")},
|
|
{QStringLiteral("عربي (ar-SA)"), QStringLiteral("ar-SA")},
|
|
{QStringLiteral("Català (ca-ES)"), QStringLiteral("ca-ES")},
|
|
{QStringLiteral("Čeština (cs-CZ)"), QStringLiteral("cs-CZ")},
|
|
{QStringLiteral("Dansk (da-DK)"), QStringLiteral("da-DK")},
|
|
{QStringLiteral("Deutsch (de-DE)"), QStringLiteral("de-DE")},
|
|
{QStringLiteral("Ελληνικά (el-GR)"), QStringLiteral("el-GR")},
|
|
{QStringLiteral("English (en)"), QStringLiteral("en-US")},
|
|
{QStringLiteral("Español (Hispanoamérica) (es-419)"), QStringLiteral("es-419")},
|
|
{QStringLiteral("Español (España) (es-ES)"), QStringLiteral("es-ES")},
|
|
{QStringLiteral("فارسی (fa-IR)"), QStringLiteral("fa-IR")},
|
|
{QStringLiteral("Suomi (fi-FI)"), QStringLiteral("fi-FI")},
|
|
{QStringLiteral("Français (fr-FR)"), QStringLiteral("fr-FR")},
|
|
{QStringLiteral("עִבְרִית (he-IL)"), QStringLiteral("he-IL")},
|
|
{QStringLiteral("मानक हिन्दी (hi-IN)"), QStringLiteral("hi-IN")},
|
|
{QStringLiteral("Magyar (hu-HU)"), QStringLiteral("hu-HU")},
|
|
{QStringLiteral("hrvatski (hr-HR)"), QStringLiteral("hr-HR")},
|
|
{QStringLiteral("Bahasa Indonesia (id-ID)"), QStringLiteral("id-ID")},
|
|
{QStringLiteral("Italiano (it-IT)"), QStringLiteral("it-IT")},
|
|
{QStringLiteral("日本語 (ja-JP)"), QStringLiteral("ja-JP")},
|
|
{QStringLiteral("한국어 (ko-KR)"), QStringLiteral("ko-KR")},
|
|
{QStringLiteral("Latvija (lv-LV)"), QStringLiteral("lv-LV")},
|
|
{QStringLiteral("Lietuvių (lt-LT)"), QStringLiteral("lt-LT")},
|
|
{QStringLiteral("Nederlands (nl-NL)"), QStringLiteral("nl-NL")},
|
|
{QStringLiteral("Norsk (no-NO)"), QStringLiteral("no-NO")},
|
|
{QStringLiteral("Polski (pl-PL)"), QStringLiteral("pl-PL")},
|
|
{QStringLiteral("Português (Brasil) (pt-BR)"), QStringLiteral("pt-BR")},
|
|
{QStringLiteral("Português (Portugal) (pt-PT)"), QStringLiteral("pt-PT")},
|
|
{QStringLiteral("Limba română (ro-RO)"), QStringLiteral("ro-RO")},
|
|
{QStringLiteral("Русский (ru-RU)"), QStringLiteral("ru-RU")},
|
|
{QStringLiteral("Српски језик (sr-SP)"), QStringLiteral("sr-SP")},
|
|
{QStringLiteral("Svenska (sv-SE)"), QStringLiteral("sv-SE")},
|
|
{QStringLiteral("Türkçe (tr-TR)"), QStringLiteral("tr-TR")},
|
|
{QStringLiteral("Українська мова (uk-UA)"), QStringLiteral("uk-UA")},
|
|
{QStringLiteral("Tiếng Việt (vi-VN)"), QStringLiteral("vi-VN")},
|
|
{QStringLiteral("简体中文 (zh-CN)"), QStringLiteral("zh-CN")},
|
|
{QStringLiteral("繁體中文 (zh-TW)"), QStringLiteral("zh-TW")},
|
|
};
|
|
}
|
|
|
|
void QtHost::UpdateGlyphRangesAndClearCache(QWidget* dialog_parent, const std::string_view language)
|
|
{
|
|
const FontInfo* fi = GetFontInfo(language);
|
|
|
|
const char* imgui_font_name = nullptr;
|
|
|
|
if (fi)
|
|
imgui_font_name = fi->imgui_font_name;
|
|
|
|
// Check for the presence of font files.
|
|
std::string font_path;
|
|
if (imgui_font_name)
|
|
{
|
|
// Non-standard fonts always go to the user resources directory, since they're downloaded on demand.
|
|
font_path = Path::Combine(EmuFolders::UserResources,
|
|
SmallString::from_format("fonts" FS_OSPATH_SEPARATOR_STR "{}", imgui_font_name));
|
|
if (!DownloadMissingFont(dialog_parent, imgui_font_name, font_path))
|
|
font_path.clear();
|
|
}
|
|
if (font_path.empty())
|
|
{
|
|
// Use the default font.
|
|
font_path = EmuFolders::GetOverridableResourcePath(SmallString::from_format(
|
|
"fonts" FS_OSPATH_SEPARATOR_STR "{}", DEFAULT_IMGUI_FONT_NAME));
|
|
}
|
|
|
|
// Called on UI thread, so we need to do this on the CPU/GS thread if it's active.
|
|
if (g_emu_thread)
|
|
{
|
|
Host::RunOnCPUThread([font_path = std::move(font_path)]() mutable {
|
|
if (MTGS::IsOpen())
|
|
{
|
|
MTGS::RunOnGSThread([font_path = std::move(font_path)]() mutable {
|
|
ImGuiManager::SetFontPath(std::move(font_path));
|
|
});
|
|
}
|
|
else
|
|
{
|
|
ImGuiManager::SetFontPath(std::move(font_path));
|
|
}
|
|
|
|
Host::ClearTranslationCache();
|
|
});
|
|
}
|
|
else
|
|
{
|
|
// Startup, safe to set directly.
|
|
ImGuiManager::SetFontPath(std::move(font_path));
|
|
Host::ClearTranslationCache();
|
|
}
|
|
}
|
|
|
|
bool QtHost::DownloadMissingFont(QWidget* dialog_parent, const char* font_name, const std::string& path)
|
|
{
|
|
if (FileSystem::FileExists(path.c_str()))
|
|
return true;
|
|
|
|
{
|
|
QMessageBox msgbox(dialog_parent);
|
|
msgbox.setWindowTitle(qApp->translate("MainWindow", "Missing Font File"));
|
|
msgbox.setWindowIcon(QtHost::GetAppIcon());
|
|
msgbox.setWindowModality(Qt::WindowModal);
|
|
msgbox.setIcon(QMessageBox::Critical);
|
|
msgbox.setTextFormat(Qt::RichText);
|
|
msgbox.setText(qApp->translate("MainWindow",
|
|
"The font file '%1' is required for the On-Screen Display and Big Picture Mode to show messages in your language.<br><br>"
|
|
"Do you want to download this file now? These files are usually less than 10 megabytes in size.<br><br>"
|
|
"<strong>If you do not download this file, on-screen messages will not be readable.</strong>")
|
|
.arg(QLatin1StringView(font_name)));
|
|
msgbox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
|
if (msgbox.exec() != QMessageBox::Yes)
|
|
return false;
|
|
}
|
|
|
|
const QString progress_title = qApp->translate("MainWindow", "Downloading Files");
|
|
std::string url = QtHost::GetRuntimeDownloadedResourceURL(font_name);
|
|
return QtHost::DownloadFile(dialog_parent, progress_title, std::move(url), path);
|
|
}
|
|
|
|
static constexpr const QtHost::FontInfo s_font_info[] = {
|
|
{"ar-SA", "NotoSansArabic-Regular.ttf"},
|
|
{"fa-IR", "NotoSansArabic-Regular.ttf"},
|
|
{"hi-IN", "NotoSansDevanagari-Regular.ttf"},
|
|
{"he-IL", "NotoSansHebrew-Regular.ttf"},
|
|
{"ja-JP", "NotoSansJP-Regular.ttf"},
|
|
{"ko-KR", "NotoSansKR-Regular.ttf"},
|
|
{"zh-CN", "NotoSansSC-Regular.ttf"},
|
|
{"zh-TW", "NotoSansTC-Regular.ttf"},
|
|
};
|
|
|
|
const QtHost::FontInfo* QtHost::GetFontInfo(const std::string_view language)
|
|
{
|
|
for (const FontInfo& it : s_font_info)
|
|
{
|
|
if (language == it.language)
|
|
return ⁢
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
int QtHost::LocaleSensitiveCompare(QStringView lhs, QStringView rhs)
|
|
{
|
|
return s_current_collator.compare(lhs, rhs);
|
|
}
|