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"