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 | |