| 1 | /* |
| 2 | * SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org> |
| 3 | * SPDX-FileCopyrightText: 2012 Martin Sandsmark <martin.sandsmark@kde.org> |
| 4 | * |
| 5 | * SPDX-License-Identifier: LGPL-2.1-or-later |
| 6 | */ |
| 7 | #include "client_p.h" |
| 8 | #include "loader_p.h" |
| 9 | #include "settingsimpl_p.h" |
| 10 | #include "spellerplugin_p.h" |
| 11 | |
| 12 | #include "core_debug.h" |
| 13 | |
| 14 | #include <QCoreApplication> |
| 15 | #include <QDir> |
| 16 | #include <QHash> |
| 17 | #include <QList> |
| 18 | #include <QLocale> |
| 19 | #include <QMap> |
| 20 | #include <QPluginLoader> |
| 21 | |
| 22 | #include <algorithm> |
| 23 | |
| 24 | namespace Sonnet |
| 25 | { |
| 26 | class LoaderPrivate |
| 27 | { |
| 28 | public: |
| 29 | SettingsImpl *settings; |
| 30 | |
| 31 | // <language, Clients with that language > |
| 32 | QMap<QString, QList<Client *>> languageClients; |
| 33 | QStringList clients; |
| 34 | |
| 35 | QSet<QString> loadedPlugins; |
| 36 | |
| 37 | QStringList languagesNameCache; |
| 38 | QHash<QString, QSharedPointer<SpellerPlugin>> spellerCache; |
| 39 | }; |
| 40 | |
| 41 | Q_GLOBAL_STATIC(Loader, s_loader) |
| 42 | |
| 43 | Loader *Loader::openLoader() |
| 44 | { |
| 45 | if (s_loader.isDestroyed()) { |
| 46 | return nullptr; |
| 47 | } |
| 48 | |
| 49 | return s_loader(); |
| 50 | } |
| 51 | |
| 52 | Loader::Loader() |
| 53 | : d(new LoaderPrivate) |
| 54 | { |
| 55 | d->settings = new SettingsImpl(this); |
| 56 | d->settings->restore(); |
| 57 | loadPlugins(); |
| 58 | } |
| 59 | |
| 60 | Loader::~Loader() |
| 61 | { |
| 62 | qCDebug(SONNET_LOG_CORE) << "Removing loader: " << this; |
| 63 | delete d->settings; |
| 64 | d->settings = nullptr; |
| 65 | } |
| 66 | |
| 67 | SpellerPlugin *Loader::createSpeller(const QString &language, const QString &clientName) const |
| 68 | { |
| 69 | QString backend = clientName; |
| 70 | QString plang = language; |
| 71 | |
| 72 | if (plang.isEmpty()) { |
| 73 | plang = d->settings->defaultLanguage(); |
| 74 | } |
| 75 | |
| 76 | auto clientsItr = d->languageClients.constFind(key: plang); |
| 77 | if (clientsItr == d->languageClients.constEnd()) { |
| 78 | if (language.isEmpty() || language == QStringLiteral("C" )) { |
| 79 | qCDebug(SONNET_LOG_CORE) << "No language dictionaries for the language:" << plang << "trying to load en_US as default" ; |
| 80 | return createSpeller(QStringLiteral("en_US" ), clientName); |
| 81 | } |
| 82 | qCDebug(SONNET_LOG_CORE) << "No language dictionaries for the language:" << plang; |
| 83 | Q_EMIT loadingDictionaryFailed(language: plang); |
| 84 | return nullptr; |
| 85 | } |
| 86 | |
| 87 | const QList<Client *> lClients = *clientsItr; |
| 88 | |
| 89 | if (backend.isEmpty()) { |
| 90 | backend = d->settings->defaultClient(); |
| 91 | if (!backend.isEmpty()) { |
| 92 | // check if the default client supports the requested language; |
| 93 | // if it does it will be an element of lClients. |
| 94 | bool unknown = !std::any_of(first: lClients.constBegin(), last: lClients.constEnd(), pred: [backend](const Client *client) { |
| 95 | return client->name() == backend; |
| 96 | }); |
| 97 | if (unknown) { |
| 98 | qCWarning(SONNET_LOG_CORE) << "Default client" << backend << "doesn't support language:" << plang; |
| 99 | backend = QString(); |
| 100 | } |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | QListIterator<Client *> itr(lClients); |
| 105 | while (itr.hasNext()) { |
| 106 | Client *item = itr.next(); |
| 107 | if (!backend.isEmpty()) { |
| 108 | if (backend == item->name()) { |
| 109 | SpellerPlugin *dict = item->createSpeller(language: plang); |
| 110 | qCDebug(SONNET_LOG_CORE) << "Using the" << item->name() << "plugin for language" << plang; |
| 111 | return dict; |
| 112 | } |
| 113 | } else { |
| 114 | // the first one is the one with the highest |
| 115 | // reliability |
| 116 | SpellerPlugin *dict = item->createSpeller(language: plang); |
| 117 | qCDebug(SONNET_LOG_CORE) << "Using the" << item->name() << "plugin for language" << plang; |
| 118 | return dict; |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | qCWarning(SONNET_LOG_CORE) << "The default client" << backend << "has no language dictionaries for the language:" << plang; |
| 123 | return nullptr; |
| 124 | } |
| 125 | |
| 126 | QSharedPointer<SpellerPlugin> Loader::cachedSpeller(const QString &language) |
| 127 | { |
| 128 | auto &speller = d->spellerCache[language]; |
| 129 | if (!speller) { |
| 130 | speller.reset(t: createSpeller(language)); |
| 131 | } |
| 132 | return speller; |
| 133 | } |
| 134 | |
| 135 | void Loader::clearSpellerCache() |
| 136 | { |
| 137 | d->spellerCache.clear(); |
| 138 | } |
| 139 | |
| 140 | QStringList Loader::clients() const |
| 141 | { |
| 142 | return d->clients; |
| 143 | } |
| 144 | |
| 145 | QStringList Loader::languages() const |
| 146 | { |
| 147 | return d->languageClients.keys(); |
| 148 | } |
| 149 | |
| 150 | QString Loader::languageNameForCode(const QString &langCode) const |
| 151 | { |
| 152 | QString currentDictionary = langCode; // e.g. en_GB-ize-wo_accents |
| 153 | QString isoCode; // locale ISO name |
| 154 | QString variantName; // dictionary variant name e.g. w_accents |
| 155 | QString localizedLang; // localized language |
| 156 | QString localizedCountry; // localized country |
| 157 | QString localizedVariant; |
| 158 | QByteArray variantEnglish; // dictionary variant in English |
| 159 | |
| 160 | int minusPos; // position of "-" char |
| 161 | int variantCount = 0; // used to iterate over variantList |
| 162 | |
| 163 | struct variantListType { |
| 164 | const char *variantShortName; |
| 165 | const char *variantEnglishName; |
| 166 | }; |
| 167 | |
| 168 | /* |
| 169 | * This redefines the QT_TRANSLATE_NOOP3 macro provided by Qt to indicate that |
| 170 | * statically initialised text should be translated so that it expands to just |
| 171 | * the string that should be translated, making it possible to use it in the |
| 172 | * single string construct below. |
| 173 | */ |
| 174 | #undef QT_TRANSLATE_NOOP3 |
| 175 | #define QT_TRANSLATE_NOOP3(a, b, c) b |
| 176 | |
| 177 | const variantListType variantList[] = {{.variantShortName: "40" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "40" , "dictionary variant" )}, // what does 40 mean? |
| 178 | {.variantShortName: "60" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "60" , "dictionary variant" )}, // what does 60 mean? |
| 179 | {.variantShortName: "80" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "80" , "dictionary variant" )}, // what does 80 mean? |
| 180 | {.variantShortName: "ise" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "-ise suffixes" , "dictionary variant" )}, |
| 181 | {.variantShortName: "ize" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "-ize suffixes" , "dictionary variant" )}, |
| 182 | {.variantShortName: "ise-w_accents" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "-ise suffixes and with accents" , "dictionary variant" )}, |
| 183 | {.variantShortName: "ise-wo_accents" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "-ise suffixes and without accents" , "dictionary variant" )}, |
| 184 | {.variantShortName: "ize-w_accents" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "-ize suffixes and with accents" , "dictionary variant" )}, |
| 185 | {.variantShortName: "ize-wo_accents" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "-ize suffixes and without accents" , "dictionary variant" )}, |
| 186 | {.variantShortName: "lrg" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "large" , "dictionary variant" )}, |
| 187 | {.variantShortName: "med" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "medium" , "dictionary variant" )}, |
| 188 | {.variantShortName: "sml" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "small" , "dictionary variant" )}, |
| 189 | {.variantShortName: "variant_0" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "variant 0" , "dictionary variant" )}, |
| 190 | {.variantShortName: "variant_1" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "variant 1" , "dictionary variant" )}, |
| 191 | {.variantShortName: "variant_2" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "variant 2" , "dictionary variant" )}, |
| 192 | {.variantShortName: "wo_accents" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "without accents" , "dictionary variant" )}, |
| 193 | {.variantShortName: "w_accents" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "with accents" , "dictionary variant" )}, |
| 194 | {.variantShortName: "ye" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "with ye, modern russian" , "dictionary variant" )}, |
| 195 | {.variantShortName: "yeyo" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "with yeyo, modern and old russian" , "dictionary variant" )}, |
| 196 | {.variantShortName: "yo" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "with yo, old russian" , "dictionary variant" )}, |
| 197 | {.variantShortName: "extended" , QT_TRANSLATE_NOOP3("Sonnet::Loader" , "extended" , "dictionary variant" )}, |
| 198 | {.variantShortName: nullptr, .variantEnglishName: nullptr}}; |
| 199 | |
| 200 | minusPos = currentDictionary.indexOf(ch: QLatin1Char('-')); |
| 201 | if (minusPos != -1) { |
| 202 | variantName = currentDictionary.right(n: currentDictionary.length() - minusPos - 1); |
| 203 | while (variantList[variantCount].variantShortName != nullptr) { |
| 204 | if (QLatin1String(variantList[variantCount].variantShortName) == variantName) { |
| 205 | break; |
| 206 | } else { |
| 207 | variantCount++; |
| 208 | } |
| 209 | } |
| 210 | if (variantList[variantCount].variantShortName != nullptr) { |
| 211 | variantEnglish = variantList[variantCount].variantEnglishName; |
| 212 | } else { |
| 213 | variantEnglish = variantName.toLatin1(); |
| 214 | } |
| 215 | |
| 216 | localizedVariant = tr(s: variantEnglish.constData(), c: "dictionary variant" ); |
| 217 | isoCode = currentDictionary.left(n: minusPos); |
| 218 | } else { |
| 219 | isoCode = currentDictionary; |
| 220 | } |
| 221 | |
| 222 | QLocale locale(isoCode); |
| 223 | localizedCountry = locale.nativeTerritoryName(); |
| 224 | localizedLang = locale.nativeLanguageName(); |
| 225 | |
| 226 | if (localizedLang.isEmpty() && localizedCountry.isEmpty()) { |
| 227 | return isoCode; // We have nothing |
| 228 | } |
| 229 | |
| 230 | if (!localizedCountry.isEmpty() && !localizedVariant.isEmpty()) { // We have both a country name and a variant |
| 231 | return tr(s: "%1 (%2) [%3]" , c: "dictionary name; %1 = language name, %2 = country name and %3 = language variant name" ) |
| 232 | .arg(args&: localizedLang, args&: localizedCountry, args&: localizedVariant); |
| 233 | } else if (!localizedCountry.isEmpty()) { // We have a country name |
| 234 | return tr(s: "%1 (%2)" , c: "dictionary name; %1 = language name, %2 = country name" ).arg(args&: localizedLang, args&: localizedCountry); |
| 235 | } else { // We only have a language name |
| 236 | return localizedLang; |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | QStringList Loader::languageNames() const |
| 241 | { |
| 242 | /* For whatever reason languages() might change. So, |
| 243 | * to be in sync with it let's do the following check. |
| 244 | */ |
| 245 | if (d->languagesNameCache.count() == languages().count()) { |
| 246 | return d->languagesNameCache; |
| 247 | } |
| 248 | |
| 249 | QStringList allLocalizedDictionaries; |
| 250 | for (const QString &langCode : languages()) { |
| 251 | allLocalizedDictionaries.append(t: languageNameForCode(langCode)); |
| 252 | } |
| 253 | // cache the list |
| 254 | d->languagesNameCache = allLocalizedDictionaries; |
| 255 | return allLocalizedDictionaries; |
| 256 | } |
| 257 | |
| 258 | SettingsImpl *Loader::settings() const |
| 259 | { |
| 260 | return d->settings; |
| 261 | } |
| 262 | |
| 263 | void Loader::loadPlugins() |
| 264 | { |
| 265 | #ifndef SONNET_STATIC |
| 266 | const QStringList libPaths = QCoreApplication::libraryPaths() << QStringLiteral(INSTALLATION_PLUGIN_PATH); |
| 267 | const QString pathSuffix(QStringLiteral("/kf6/sonnet/" )); |
| 268 | for (const QString &libPath : libPaths) { |
| 269 | QDir dir(libPath + pathSuffix); |
| 270 | if (!dir.exists()) { |
| 271 | continue; |
| 272 | } |
| 273 | for (const QString &fileName : dir.entryList(filters: QDir::Files)) { |
| 274 | loadPlugin(pluginPath: dir.absoluteFilePath(fileName)); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | if (d->loadedPlugins.isEmpty()) { |
| 279 | qCWarning(SONNET_LOG_CORE) << "Sonnet: No speller backends available!" ; |
| 280 | } |
| 281 | #else |
| 282 | for (auto plugin : QPluginLoader::staticPlugins()) { |
| 283 | if (plugin.metaData()[QLatin1String("IID" )].toString() == QLatin1String("org.kde.sonnet.Client" )) { |
| 284 | loadPlugin(plugin); |
| 285 | } |
| 286 | } |
| 287 | #endif |
| 288 | } |
| 289 | |
| 290 | void Loader::loadPlugin(const QStaticPlugin &plugin) |
| 291 | { |
| 292 | Client *client = qobject_cast<Client *>(object: plugin.instance()); |
| 293 | if (!client) { |
| 294 | qCWarning(SONNET_LOG_CORE) << "Sonnet: Invalid static plugin loaded" << plugin.metaData(); |
| 295 | return; |
| 296 | } |
| 297 | |
| 298 | addClient(client); |
| 299 | } |
| 300 | |
| 301 | void Loader::loadPlugin(const QString &pluginPath) |
| 302 | { |
| 303 | QPluginLoader plugin(pluginPath); |
| 304 | const QString pluginId = QFileInfo(pluginPath).completeBaseName(); |
| 305 | if (!pluginId.isEmpty()) { |
| 306 | if (d->loadedPlugins.contains(value: pluginId)) { |
| 307 | qCDebug(SONNET_LOG_CORE) << "Skipping already loaded" << pluginPath; |
| 308 | return; |
| 309 | } |
| 310 | } |
| 311 | d->loadedPlugins.insert(value: pluginId); |
| 312 | |
| 313 | if (!plugin.load()) { // We do this separately for better error handling |
| 314 | qCDebug(SONNET_LOG_CORE) << "Sonnet: Unable to load plugin" << pluginPath << "Error:" << plugin.errorString(); |
| 315 | d->loadedPlugins.remove(value: pluginId); |
| 316 | return; |
| 317 | } |
| 318 | |
| 319 | Client *client = qobject_cast<Client *>(object: plugin.instance()); |
| 320 | |
| 321 | if (!client) { |
| 322 | qCWarning(SONNET_LOG_CORE) << "Sonnet: Invalid plugin loaded" << pluginPath; |
| 323 | plugin.unload(); // don't leave it in memory |
| 324 | return; |
| 325 | } |
| 326 | |
| 327 | addClient(client); |
| 328 | } |
| 329 | |
| 330 | void Loader::addClient(Client *client) |
| 331 | { |
| 332 | const QStringList languages = client->languages(); |
| 333 | d->clients.append(t: client->name()); |
| 334 | |
| 335 | for (const QString &language : languages) { |
| 336 | QList<Client *> &languageClients = d->languageClients[language]; |
| 337 | |
| 338 | if (languageClients.isEmpty() // |
| 339 | || client->reliability() < languageClients.first()->reliability()) { |
| 340 | languageClients.append(t: client); // less reliable, to the end |
| 341 | } else { |
| 342 | languageClients.prepend(t: client); // more reliable, to the front |
| 343 | } |
| 344 | } |
| 345 | } |
| 346 | |
| 347 | void Loader::changed() |
| 348 | { |
| 349 | Q_EMIT configurationChanged(); |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | #include "moc_loader_p.cpp" |
| 354 | |