File indexing completed on 2024-05-19 05:38:23

0001 /*
0002     kcmregionandlang.cpp
0003     SPDX-FileCopyrightText: 2014 Sebastian Kügler <sebas@kde.org>
0004     SPDX-FileCopyrightText: 2021 Han Young <hanyoung@protonmail.com>
0005     SPDX-FileCopyrightText: 2023 Serenity Cybersecurity, LLC <license@futurecrew.ru>
0006                                  Author: Gleb Popov <arrowd@FreeBSD.org>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "kcmregionandlang.h"
0012 
0013 #include <unistd.h>
0014 
0015 #include <QDBusConnection>
0016 #include <QDBusMessage>
0017 #include <QDBusPendingCall>
0018 #include <QDir>
0019 #include <QQuickImageProvider>
0020 
0021 #include <KCountryFlagEmojiIconEngine>
0022 #include <KLocalizedString>
0023 #include <KPluginFactory>
0024 #include <KSharedConfig>
0025 
0026 #include "languagelistmodel.h"
0027 #include "localegenerator.h"
0028 #include "localegeneratorbase.h"
0029 #include "localelistmodel.h"
0030 #include "optionsmodel.h"
0031 #include "regionandlangsettings.h"
0032 
0033 using namespace KCM_RegionAndLang;
0034 
0035 K_PLUGIN_CLASS_WITH_JSON(KCMRegionAndLang, "kcm_regionandlang.json")
0036 
0037 class FlagImageProvider : public QQuickImageProvider
0038 {
0039 public:
0040     FlagImageProvider()
0041         : QQuickImageProvider(QQuickImageProvider::Pixmap)
0042     {
0043     }
0044 
0045     QPixmap requestPixmap(const QString &id, QSize *, const QSize &requestedSize) override
0046     {
0047         CacheContext cacheContext(id, requestedSize);
0048         if (const auto it = m_pixmapCache.find(cacheContext); it != m_pixmapCache.end()) {
0049             return it.value();
0050         }
0051         return m_pixmapCache[cacheContext] = QIcon(new KCountryFlagEmojiIconEngine(id)).pixmap(requestedSize);
0052     }
0053 
0054 private:
0055     using CacheContext = std::pair<QString, QSize>;
0056     QHash<CacheContext, QPixmap> m_pixmapCache;
0057 };
0058 
0059 KCMRegionAndLang::KCMRegionAndLang(QObject *parent, const KPluginMetaData &data)
0060     : KQuickManagedConfigModule(parent, data)
0061     , m_settings(new RegionAndLangSettings(this))
0062     , m_optionsModel(new OptionsModel(this))
0063     , m_generator(LocaleGenerator::getGenerator())
0064 {
0065     connect(m_generator, &LocaleGeneratorBase::userHasToGenerateManually, this, &KCMRegionAndLang::userHasToGenerateManually);
0066     connect(m_generator, &LocaleGeneratorBase::success, this, &KCMRegionAndLang::generateFinished);
0067     connect(m_generator, &LocaleGeneratorBase::needsFont, this, &KCMRegionAndLang::requireInstallFont);
0068     connect(m_generator, &LocaleGeneratorBase::success, this, &KCMRegionAndLang::saveToConfigFile);
0069     connect(m_generator, &LocaleGeneratorBase::userHasToGenerateManually, this, &KCMRegionAndLang::saveToConfigFile);
0070     connect(m_generator, &LocaleGeneratorBase::needsFont, this, &KCMRegionAndLang::saveToConfigFile);
0071 
0072     // if we don't support auto locale generation for current system (BSD, musl etc.), userHasToGenerateManually regarded as success
0073     if (strcmp(m_generator->metaObject()->className(), "LocaleGeneratorBase") != 0) {
0074         connect(m_generator, &LocaleGeneratorBase::success, this, &KCMRegionAndLang::takeEffectNextTime);
0075     } else {
0076         connect(m_generator, &LocaleGeneratorBase::userHasToGenerateManually, this, &KCMRegionAndLang::takeEffectNextTime);
0077     }
0078 
0079     qmlRegisterAnonymousType<RegionAndLangSettings>("kcmregionandlang", 1);
0080     qmlRegisterAnonymousType<OptionsModel>("kcmregionandlang", 1);
0081     qmlRegisterAnonymousType<SelectedLanguageModel>("kcmregionandlang", 1);
0082     qmlRegisterType<LocaleListModel>("kcmregionandlang", 1, 0, "LocaleListModel");
0083     qmlRegisterType<LanguageListModel>("kcmregionandlang", 1, 0, "LanguageListModel");
0084     qRegisterMetaType<KCM_RegionAndLang::SettingType>();
0085     qmlRegisterUncreatableMetaObject(KCM_RegionAndLang::staticMetaObject, "kcmregionandlang", 1, 0, "SettingType", "Error: SettingType is an enum");
0086 
0087 #if GLIBC_LOCALE_GENERATED
0088     // fedora pre generate locales, fetch available locales from localectl. /usr/share/i18n/locales is empty in fedora
0089     QDir glibcLocaleDir(localeFileDirPath());
0090     if (glibcLocaleDir.isEmpty()) {
0091         auto localectlPath = QStandardPaths::findExecutable(QStringLiteral("localectl"));
0092         if (!localectlPath.isEmpty()) {
0093             m_localectl = new QProcess(this);
0094             m_localectl->setProgram(localectlPath);
0095             m_localectl->setArguments({QStringLiteral("list-locales")});
0096             connect(m_localectl, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
0097                 m_enabled = true; // set to true even if failed. otherwise our failed notification is also grey out
0098                 if (exitCode != 0 || status != QProcess::NormalExit) {
0099                     Q_EMIT encountedError(failedFindLocalesMessage());
0100                 }
0101                 Q_EMIT enabledChanged();
0102             });
0103             m_localectl->start();
0104         }
0105     } else {
0106         m_enabled = true;
0107     }
0108 #else
0109     m_enabled = true;
0110 #endif
0111 }
0112 
0113 QString KCMRegionAndLang::failedFindLocalesMessage()
0114 {
0115     return xi18nc("@info this will be shown as an error message",
0116                   "Could not find the system's available locales using the <command>localectl</command> tool. Please file a bug report about this at "
0117                   "<link>https://bugs.kde.org</link>");
0118 }
0119 
0120 QString KCMRegionAndLang::localeFileDirPath()
0121 {
0122     return QStringLiteral("/usr/share/i18n/locales");
0123 }
0124 
0125 void KCMRegionAndLang::save()
0126 {
0127     // assemble full locales in use
0128     QStringList locales;
0129     if (!settings()->isDefaultSetting(SettingType::Lang)) {
0130         locales.append(settings()->lang());
0131     }
0132     if (!settings()->isDefaultSetting(SettingType::Numeric)) {
0133         locales.append(settings()->numeric());
0134     }
0135     if (!settings()->isDefaultSetting(SettingType::Time)) {
0136         locales.append(settings()->time());
0137     }
0138     if (!settings()->isDefaultSetting(SettingType::Measurement)) {
0139         locales.append(settings()->measurement());
0140     }
0141     if (!settings()->isDefaultSetting(SettingType::Currency)) {
0142         locales.append(settings()->monetary());
0143     }
0144     if (!settings()->isDefaultSetting(SettingType::PaperSize)) {
0145         locales.append(settings()->paperSize());
0146     }
0147     if (!settings()->isDefaultSetting(SettingType::Address)) {
0148         locales.append(settings()->address());
0149     }
0150     if (!settings()->isDefaultSetting(SettingType::NameStyle)) {
0151         locales.append(settings()->nameStyle());
0152     }
0153     if (!settings()->isDefaultSetting(SettingType::PhoneNumbers)) {
0154         locales.append(settings()->phoneNumbers());
0155     }
0156 #ifdef GLIBC_LOCALE
0157     if (!settings()->language().isEmpty()) {
0158         QStringList languages = settings()->language().split(QLatin1Char(':'));
0159         for (const QString &lang : languages) {
0160             auto glibcLocale = toGlibcLocale(lang);
0161             if (glibcLocale.has_value()) {
0162                 locales.append(glibcLocale.value());
0163             }
0164         }
0165     }
0166 #endif
0167 
0168     auto setLangCall = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.Accounts"),
0169                                                       QStringLiteral("/org/freedesktop/Accounts/User%1").arg(getuid()),
0170                                                       QStringLiteral("org.freedesktop.Accounts.User"),
0171                                                       QStringLiteral("SetLanguage"));
0172     setLangCall.setArguments({settings()->lang()});
0173     QDBusConnection::systemBus().asyncCall(setLangCall);
0174 
0175     if (!locales.isEmpty()) {
0176         Q_EMIT startGenerateLocale();
0177         m_generator->localesGenerate(locales);
0178     } else {
0179         // probably after clicking "defaults" so all the setting is default
0180         saveToConfigFile();
0181     }
0182     Q_EMIT saveClicked();
0183 }
0184 
0185 void KCMRegionAndLang::saveToConfigFile()
0186 {
0187     KQuickManagedConfigModule::save();
0188 }
0189 
0190 RegionAndLangSettings *KCMRegionAndLang::settings() const
0191 {
0192     return m_settings;
0193 }
0194 
0195 OptionsModel *KCMRegionAndLang::optionsModel() const
0196 {
0197     return m_optionsModel;
0198 }
0199 
0200 void KCMRegionAndLang::unset(SettingType setting) const
0201 {
0202     const char *entry = nullptr;
0203     switch (setting) {
0204     case SettingType::Language:
0205         Q_ASSERT(false); // shouldn't happen
0206         return;
0207     case SettingType::Lang:
0208         entry = "LANG";
0209         settings()->setLang(settings()->defaultLangValue());
0210         break;
0211     case SettingType::Numeric:
0212         entry = "LC_NUMERIC";
0213         settings()->setNumeric(settings()->defaultNumericValue());
0214         break;
0215     case SettingType::Time:
0216         entry = "LC_TIME";
0217         settings()->setTime(settings()->defaultTimeValue());
0218         break;
0219     case SettingType::Measurement:
0220         entry = "LC_MEASUREMENT";
0221         settings()->setMeasurement(settings()->defaultMeasurementValue());
0222         break;
0223     case SettingType::Currency:
0224         entry = "LC_MONETARY";
0225         settings()->setMonetary(settings()->defaultMonetaryValue());
0226         break;
0227     case SettingType::PaperSize:
0228         entry = "LC_PAPER";
0229         settings()->setPaperSize(settings()->defaultPaperSizeValue());
0230         break;
0231     case SettingType::Address:
0232         entry = "LC_ADDRESS";
0233         settings()->setAddress(settings()->defaultAddressValue());
0234         break;
0235     case SettingType::NameStyle:
0236         entry = "LC_NAME";
0237         settings()->setNameStyle(settings()->defaultNameStyleValue());
0238         break;
0239     case SettingType::PhoneNumbers:
0240         entry = "LC_TELEPHONE";
0241         settings()->setPhoneNumbers(settings()->defaultPhoneNumbersValue());
0242         break;
0243     }
0244 
0245     settings()->config()->group(QStringLiteral("Formats")).deleteEntry(entry);
0246 }
0247 
0248 void KCMRegionAndLang::reboot()
0249 {
0250     auto method = QDBusMessage::createMethodCall(QStringLiteral("org.kde.LogoutPrompt"),
0251                                                  QStringLiteral("/LogoutPrompt"),
0252                                                  QStringLiteral("org.kde.LogoutPrompt"),
0253                                                  QStringLiteral("promptReboot"));
0254     QDBusConnection::sessionBus().asyncCall(method);
0255 }
0256 
0257 bool KCMRegionAndLang::enabled() const
0258 {
0259     return m_enabled;
0260 }
0261 
0262 #ifdef GLIBC_LOCALE
0263 std::optional<QString> KCMRegionAndLang::toGlibcLocale(const QString &lang)
0264 {
0265     static std::unordered_map<QString, QString> map = constructGlibcLocaleMap();
0266 
0267     if (map.contains(lang)) {
0268         return map[lang];
0269     }
0270     return std::nullopt;
0271 }
0272 #endif
0273 
0274 QString KCMRegionAndLang::toUTF8Locale(const QString &locale)
0275 {
0276     if (locale.contains(QLatin1String("UTF-8"))) {
0277         return locale;
0278     }
0279 
0280     if (locale.contains(QLatin1Char('@'))) {
0281         // uz_UZ@cyrillic to uz_UZ.UTF-8@cyrillic
0282         auto localeDup = locale;
0283         localeDup.replace(QLatin1Char('@'), QLatin1String(".UTF-8@"));
0284         return localeDup;
0285     }
0286 
0287     return locale + QLatin1String(".UTF-8");
0288 }
0289 
0290 #ifdef GLIBC_LOCALE
0291 std::unordered_map<QString, QString> KCMRegionAndLang::constructGlibcLocaleMap()
0292 {
0293     std::unordered_map<QString, QString> localeMap;
0294 
0295     QDir glibcLocaleDir(localeFileDirPath());
0296     auto availableLocales = glibcLocaleDir.entryList(QDir::Files);
0297     // not glibc system or corrupted system
0298     if (availableLocales.isEmpty()) {
0299         if (m_localectl) {
0300             availableLocales = QString(m_localectl->readAllStandardOutput()).split('\n');
0301         }
0302         if (availableLocales.isEmpty()) {
0303             Q_EMIT encountedError(failedFindLocalesMessage());
0304             return localeMap;
0305         }
0306     }
0307 
0308     // map base locale code to actual glibc locale filename: "en" => ["en_US", "en_GB"]
0309     std::unordered_map<QString, std::vector<QString>> baseLocaleMap(availableLocales.size());
0310     for (const auto &glibcLocale : availableLocales) {
0311         // we want only absolute base locale code, for sr@ijekavian and en_US, we get sr and en
0312         auto baseLocale = glibcLocale.split('_')[0].split('@')[0];
0313         if (baseLocaleMap.contains(baseLocale)) {
0314             baseLocaleMap[baseLocale].push_back(glibcLocale);
0315         } else {
0316             baseLocaleMap.insert({baseLocale, {glibcLocale}});
0317         }
0318     }
0319 
0320     const auto addToMap = [&localeMap](const QString &plasmaLocale, const QString &glibcLocale) {
0321         // We map the locale name and the plasma name to a valid locale. This gives us flexibility for resolution
0322         // as we can resolve pt=>pt_PT but also pt_PT=>pt_PT.
0323         // https://mail.kde.org/pipermail/kde-i18n-doc/2023-January/001340.html
0324         // https://bugs.kde.org/show_bug.cgi?id=478120
0325         localeMap.insert({glibcLocale, toUTF8Locale(glibcLocale)});
0326         localeMap.insert({plasmaLocale, toUTF8Locale(glibcLocale)});
0327     };
0328 
0329     auto plasmaLocales = KLocalizedString::availableDomainTranslations(QByteArrayLiteral("plasmashell")).values();
0330     for (const auto &plasmaLocale : plasmaLocales) {
0331         auto baseLocale = plasmaLocale.split('_')[0].split('@')[0];
0332         if (baseLocaleMap.contains(baseLocale)) {
0333             const auto &prefixedLocales = baseLocaleMap[baseLocale];
0334 
0335             // if we have one to one match, use that. Eg. en_US to en_US
0336             auto fullMatch = std::find(prefixedLocales.begin(), prefixedLocales.end(), plasmaLocale);
0337             if (fullMatch != prefixedLocales.end()) {
0338                 addToMap(plasmaLocale, *fullMatch);
0339                 continue;
0340             }
0341 
0342             // language name with same country code has higher priority, eg. es_ES > es_PA, de_DE > de_DE@euro
0343             auto mainLocale = plasmaLocale + "_" + plasmaLocale.toUpper();
0344             fullMatch = std::find(prefixedLocales.begin(), prefixedLocales.end(), mainLocale);
0345             if (fullMatch != prefixedLocales.end()) {
0346                 addToMap(plasmaLocale, *fullMatch);
0347                 continue;
0348             }
0349 
0350             // we try to match the locale with least characters diff (compare language code with country code, "sv" with "SE".lower() for "sv_SE"),
0351             // so ca@valencia matches with ca_ES@valencia
0352             // bad case: ca matches with ca_AD but not ca_ES
0353             int closestMatchIndex = 0;
0354             float minDiffPercentage = 1.0;
0355             std::array<int, 255> frequencyMap = {0};
0356             for (QChar c : plasmaLocale) {
0357                 // to lower so "sv_SE" has higher priority than "sv_FI" for language "sv"
0358                 frequencyMap[int(c.toLower().toLatin1())]++;
0359             }
0360 
0361             int i = 0;
0362             for (const auto &glibcLocale : prefixedLocales) {
0363                 auto duplicated = frequencyMap;
0364                 auto skipBase = baseLocale.size() + 1; // we skip "sv_" part of "sv_SE", eg. compare "SE" part with "sv"
0365                 for (QChar c : glibcLocale) {
0366                     if (skipBase--) {
0367                         continue;
0368                     }
0369                     duplicated[int(c.toLower().toLatin1())]--;
0370                 }
0371                 int diffChars = std::reduce(duplicated.begin(), duplicated.end(), 0, [](int sum, int diff) {
0372                     return sum + std::abs(diff);
0373                 });
0374                 float diffPercentage = float(diffChars) / glibcLocale.size();
0375                 if (diffPercentage < minDiffPercentage) {
0376                     minDiffPercentage = diffPercentage;
0377                     closestMatchIndex = i;
0378                 }
0379                 i++;
0380             }
0381             addToMap(plasmaLocale, prefixedLocales[closestMatchIndex]);
0382         }
0383     }
0384     return localeMap;
0385 }
0386 #endif
0387 
0388 void KCMRegionAndLang::load()
0389 {
0390     engine()->addImageProvider("flags", new FlagImageProvider);
0391 }
0392 
0393 #include "kcmregionandlang.moc"
0394 #include "moc_kcmregionandlang.cpp"