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 | |
31 | namespace Sonnet |
32 | { |
33 | class LoaderPrivate |
34 | { |
35 | public: |
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 | |
48 | Q_GLOBAL_STATIC(Loader, s_loader) |
49 | |
50 | Loader *Loader::openLoader() |
51 | { |
52 | if (s_loader.isDestroyed()) { |
53 | return nullptr; |
54 | } |
55 | |
56 | return s_loader(); |
57 | } |
58 | |
59 | Loader::Loader() |
60 | : d(new LoaderPrivate) |
61 | { |
62 | d->settings = new SettingsImpl(this); |
63 | d->settings->restore(); |
64 | loadPlugins(); |
65 | } |
66 | |
67 | Loader::~Loader() |
68 | { |
69 | qCDebug(SONNET_LOG_CORE) << "Removing loader: " << this; |
70 | delete d->settings; |
71 | d->settings = nullptr; |
72 | } |
73 | |
74 | SpellerPlugin *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 | |
133 | QSharedPointer<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 | |
142 | void Loader::clearSpellerCache() |
143 | { |
144 | d->spellerCache.clear(); |
145 | } |
146 | |
147 | QStringList Loader::clients() const |
148 | { |
149 | return d->clients; |
150 | } |
151 | |
152 | QStringList Loader::languages() const |
153 | { |
154 | return d->languageClients.keys(); |
155 | } |
156 | |
157 | QString 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 | |
247 | QStringList 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 | |
265 | SettingsImpl *Loader::settings() const |
266 | { |
267 | return d->settings; |
268 | } |
269 | |
270 | void 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 | |
296 | void 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 | |
348 | void Loader::changed() |
349 | { |
350 | Q_EMIT configurationChanged(); |
351 | } |
352 | } |
353 | |
354 | #include "moc_loader_p.cpp" |
355 | |