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

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