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
24namespace Sonnet
25{
26class LoaderPrivate
27{
28public:
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
41Q_GLOBAL_STATIC(Loader, s_loader)
42
43Loader *Loader::openLoader()
44{
45 if (s_loader.isDestroyed()) {
46 return nullptr;
47 }
48
49 return s_loader();
50}
51
52Loader::Loader()
53 : d(new LoaderPrivate)
54{
55 d->settings = new SettingsImpl(this);
56 d->settings->restore();
57 loadPlugins();
58}
59
60Loader::~Loader()
61{
62 qCDebug(SONNET_LOG_CORE) << "Removing loader: " << this;
63 delete d->settings;
64 d->settings = nullptr;
65}
66
67SpellerPlugin *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
126QSharedPointer<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
135void Loader::clearSpellerCache()
136{
137 d->spellerCache.clear();
138}
139
140QStringList Loader::clients() const
141{
142 return d->clients;
143}
144
145QStringList Loader::languages() const
146{
147 return d->languageClients.keys();
148}
149
150QString 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
240QStringList 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
258SettingsImpl *Loader::settings() const
259{
260 return d->settings;
261}
262
263void 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
290void 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
301void 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
330void 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
347void Loader::changed()
348{
349 Q_EMIT configurationChanged();
350}
351}
352
353#include "moc_loader_p.cpp"
354

source code of sonnet/src/core/loader.cpp