QtSpell 1.0.2
Spell checking for Qt text widgets
Checker.cpp
1/* QtSpell - Spell checking for Qt text widgets.
2 * Copyright (c) 2014-2022 Sandro Mani
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
19#include "QtSpell.hpp"
20#include "Checker_p.hpp"
21#include "Codetable.hpp"
22
23#include <enchant++.h>
24#include <QActionGroup>
25#include <QApplication>
26#include <QLibraryInfo>
27#include <QLocale>
28#include <QMenu>
29#include <QTranslator>
30#include <QtDebug>
31
32static void dict_describe_cb(const char* const lang_tag,
33 const char* const /*provider_name*/,
34 const char* const /*provider_desc*/,
35 const char* const /*provider_file*/,
36 void* user_data)
37{
38 QList<QString>* languages = static_cast<QList<QString>*>(user_data);
39 languages->append(lang_tag);
40}
41
42static enchant::Broker* get_enchant_broker() {
43#ifdef QTSPELL_ENCHANT2
44 static enchant::Broker broker;
45 return &broker;
46#else
47 return enchant::Broker::instance();
48#endif
49}
50
51
52class TranslationsInit {
53public:
54 TranslationsInit(){
55#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
56 QString translationsPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
57#else
58 QString translationsPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
59#endif
60#ifdef Q_OS_WIN
61 QDir packageDir = QDir(QString("%1/../").arg(QApplication::applicationDirPath()));
62#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
63 translationsPath = packageDir.absolutePath() + translationsPath.mid(QLibraryInfo::path(QLibraryInfo::PrefixPath).length());
64#else
65 translationsPath = packageDir.absolutePath() + translationsPath.mid(QLibraryInfo::location(QLibraryInfo::PrefixPath).length());
66#endif
67#endif
68 bool success = spellTranslator.load("QtSpell_" + QLocale::system().name(), translationsPath);
69 Q_UNUSED(success)
70 QApplication::instance()->installTranslator(&spellTranslator);
71 }
72private:
73 QTranslator spellTranslator;
74};
75
76
77namespace QtSpell {
78
79CheckerPrivate::CheckerPrivate()
80{
81}
82
83CheckerPrivate::~CheckerPrivate()
84{
85 delete speller;
86}
87
88void CheckerPrivate::init()
89{
90 static TranslationsInit tsInit;
91 Q_UNUSED(tsInit);
92
93 setLanguageInternal("");
94}
95
96bool checkLanguageInstalled(const QString &lang)
97{
98 return get_enchant_broker()->dict_exists(lang.toStdString());
99}
100
101Checker::Checker(QObject* parent)
102 : QObject(parent)
103 , d_ptr(new CheckerPrivate())
104{
105 d_ptr->q_ptr = this;
106 d_ptr->init();
107}
108
109Checker::Checker(CheckerPrivate& dd, QObject* parent)
110 : QObject(parent)
111 , d_ptr(&dd)
112{
113 d_ptr->q_ptr = this;
114 d_ptr->init();
115}
116
118{
119 delete d_ptr;
120}
121
122bool Checker::setLanguage(const QString &lang)
123{
124 Q_D(Checker);
125 bool success = d->setLanguageInternal(lang);
126 if(isAttached()){
128 }
129 return success;
130}
131
132QString Checker::getLanguage() const
133{
134 Q_D(const Checker);
135 return d->lang;
136}
137
138bool CheckerPrivate::setLanguageInternal(const QString &newLang)
139{
140 delete speller;
141 speller = nullptr;
142 lang = newLang;
143
144 // Determine language from system locale
145 if(lang.isEmpty()){
146 lang = QLocale::system().name();
147 if(lang.toLower() == "c" || lang.isEmpty()){
148 qWarning() << "Cannot use system locale " << lang;
149 lang = QString();
150 return false;
151 }
152 }
153
154 // Request dictionary
155 try {
156 speller = get_enchant_broker()->request_dict(lang.toStdString());
157 } catch(enchant::Exception& e) {
158 qWarning() << "Failed to load dictionary: " << e.what();
159 lang = QString();
160 return false;
161 }
162
163 return true;
164}
165
167{
168 Q_D(Checker);
169 d->decodeCodes = decode;
170}
171
173{
174 Q_D(const Checker);
175 return d->decodeCodes;
176}
177
179{
180 Q_D(Checker);
181 d->spellingCheckbox = show;
182}
183
185{
186 Q_D(const Checker);
187 return d->spellingCheckbox;
188}
189
191{
192 Q_D(const Checker);
193 return d->spellingEnabled;
194}
195
196void Checker::addWordToDictionary(const QString &word)
197{
198 Q_D(Checker);
199 if(d->speller){
200 d->speller->add(word.toUtf8().data());
201 }
202}
203
204bool Checker::checkWord(const QString &word) const
205{
206 Q_D(const Checker);
207 if(!d->speller || !d->spellingEnabled){
208 return true;
209 }
210 // Skip empty strings and single characters
211 if(word.length() < 2){
212 return true;
213 }
214 try{
215 return d->speller->check(word.toUtf8().data());
216 }catch(const enchant::Exception&){
217 return true;
218 }
219}
220
221void Checker::ignoreWord(const QString &word) const
222{
223 Q_D(const Checker);
224 d->speller->add_to_session(word.toUtf8().data());
225}
226
227QList<QString> Checker::getSpellingSuggestions(const QString& word) const
228{
229 Q_D(const Checker);
230 QList<QString> list;
231 if(d->speller){
232 std::vector<std::string> suggestions;
233 d->speller->suggest(word.toUtf8().data(), suggestions);
234 for(std::size_t i = 0, n = suggestions.size(); i < n; ++i){
235 list.append(QString::fromUtf8(suggestions[i].c_str()));
236 }
237 }
238 return list;
239}
240
242{
243 enchant::Broker* broker = get_enchant_broker();
244 QList<QString> languages;
245 broker->list_dicts(dict_describe_cb, &languages);
246 std::sort(languages.begin(), languages.end());
247 return languages;
248}
249
250QString Checker::decodeLanguageCode(const QString &lang)
251{
252 QString language, country, extra;
253 Codetable::instance()->lookup(lang, language, country, extra);
254 if(!country.isEmpty()){
255 QString decoded = QString("%1 (%2)").arg(language, country);
256 if(!extra.isEmpty()) {
257 decoded += QString(" [%1]").arg(extra);
258 }
259 return decoded;
260 }else{
261 return language;
262 }
263}
264
266{
267 Q_D(Checker);
268 d->spellingEnabled = enabled;
270}
271
272void Checker::showContextMenu(QMenu* menu, const QPoint& pos, int wordPos)
273{
274 Q_D(Checker);
275 QAction* insertPos = menu->actions().first();
276 if(d->speller && d->spellingEnabled){
277 QString word = getWord(wordPos);
278
279 if(!checkWord(word)) {
280 QList<QString> suggestions = getSpellingSuggestions(word);
281 if(!suggestions.isEmpty()){
282 for(int i = 0, n = qMin(10, suggestions.length()); i < n; ++i){
283 QAction* action = new QAction(suggestions[i], menu);
284 action->setProperty("wordPos", wordPos);
285 action->setProperty("suggestion", suggestions[i]);
286 connect(action, &QAction::triggered, this, &Checker::slotReplaceWord);
287 menu->insertAction(insertPos, action);
288 }
289 if(suggestions.length() > 10) {
290 QMenu* moreMenu = new QMenu();
291 for(int i = 10, n = suggestions.length(); i < n; ++i){
292 QAction* action = new QAction(suggestions[i], moreMenu);
293 action->setProperty("wordPos", wordPos);
294 action->setProperty("suggestion", suggestions[i]);
295 connect(action, &QAction::triggered, this, &Checker::slotReplaceWord);
296 moreMenu->addAction(action);
297 }
298 QAction* action = new QAction(tr("More..."), menu);
299 menu->insertAction(insertPos, action);
300 action->setMenu(moreMenu);
301 }
302 menu->insertSeparator(insertPos);
303 }
304
305 QAction* addAction = new QAction(tr("Add \"%1\" to dictionary").arg(word), menu);
306 addAction->setData(wordPos);
307 connect(addAction, &QAction::triggered, this, &Checker::slotAddWord);
308 menu->insertAction(insertPos, addAction);
309
310 QAction* ignoreAction = new QAction(tr("Ignore \"%1\"").arg(word), menu);
311 ignoreAction->setData(wordPos);
312 connect(ignoreAction, &QAction::triggered, this, &Checker::slotIgnoreWord);
313 menu->insertAction(insertPos, ignoreAction);
314 menu->insertSeparator(insertPos);
315 }
316 }
317 if(d->spellingCheckbox){
318 QAction* action = new QAction(tr("Check spelling"), menu);
319 action->setCheckable(true);
320 action->setChecked(d->spellingEnabled);
321 connect(action, &QAction::toggled, this, &Checker::setSpellingEnabled);
322 menu->insertAction(insertPos, action);
323 }
324 if(d->speller && d->spellingEnabled){
325 QMenu* languagesMenu = new QMenu();
326 QActionGroup* actionGroup = new QActionGroup(languagesMenu);
327 foreach(const QString& lang, getLanguageList()){
328 QString text = getDecodeLanguageCodes() ? decodeLanguageCode(lang) : lang;
329 QAction* action = new QAction(text, languagesMenu);
330 action->setData(lang);
331 action->setCheckable(true);
332 action->setChecked(lang == getLanguage());
333 connect(action, &QAction::triggered, this, &Checker::slotSetLanguage);
334 languagesMenu->addAction(action);
335 actionGroup->addAction(action);
336 }
337 QAction* langsAction = new QAction(tr("Languages"), menu);
338 langsAction->setMenu(languagesMenu);
339 menu->insertAction(insertPos, langsAction);
340 menu->insertSeparator(insertPos);
341 }
342
343 menu->exec(pos);
344 delete menu;
345}
346
347void Checker::slotAddWord()
348{
349 int wordPos = qobject_cast<QAction*>(QObject::sender())->data().toInt();
350 int start, end;
351 addWordToDictionary(getWord(wordPos, &start, &end));
352 checkSpelling(start, end);
353}
354
355void Checker::slotIgnoreWord()
356{
357 int wordPos = qobject_cast<QAction*>(QObject::sender())->data().toInt();
358 int start, end;
359 ignoreWord(getWord(wordPos, &start, &end));
360 checkSpelling(start, end);
361}
362
363void Checker::slotReplaceWord()
364{
365 QAction* action = qobject_cast<QAction*>(QObject::sender());
366 int wordPos = action->property("wordPos").toInt();
367 int start, end;
368 getWord(wordPos, &start, &end);
369 insertWord(start, end, action->property("suggestion").toString());
370}
371
372void Checker::slotSetLanguage(bool checked)
373{
374 if(checked) {
375 QAction* action = qobject_cast<QAction*>(QObject::sender());
376 QString lang = action->data().toString();
377 if(!setLanguage(lang)){
378 action->setChecked(false);
379 lang = "";
380 }
381 emit languageChanged(lang);
382 }
383}
384
385} // QtSpell
An abstract class providing spell checking support.
Definition QtSpell.hpp:50
void setShowCheckSpellingCheckbox(bool show)
Set whether to display an "Check spelling" checkbox in the UI.
Definition Checker.cpp:178
Checker(QObject *parent=0)
QtSpell::Checker object constructor.
Definition Checker.cpp:101
bool setLanguage(const QString &lang)
Set the spell checking language.
Definition Checker.cpp:122
virtual bool isAttached() const =0
Returns whether a widget is attached to the checker.
void setDecodeLanguageCodes(bool decode)
Set whether to decode language codes in the UI.
Definition Checker.cpp:166
QList< QString > getSpellingSuggestions(const QString &word) const
Retreive a list of spelling suggestions for the misspelled word.
Definition Checker.cpp:227
static QList< QString > getLanguageList()
Requests the list of languages available for spell checking.
Definition Checker.cpp:241
bool getShowCheckSpellingCheckbox() const
Return whether a "Check spelling" checkbox is displayed in the UI.
Definition Checker.cpp:184
bool getSpellingEnabled() const
Return whether spellchecking is performed.
Definition Checker.cpp:190
virtual ~Checker()
QtSpell::Checker object destructor.
Definition Checker.cpp:117
bool getDecodeLanguageCodes() const
Return whether langauge codes are decoded in the UI.
Definition Checker.cpp:172
void setSpellingEnabled(bool enabled)
Set whether spell checking should be performed.
Definition Checker.cpp:265
bool checkWord(const QString &word) const
Check the specified word.
Definition Checker.cpp:204
void languageChanged(const QString &newLang)
This signal is emitted when the user selects a new language from the spellchecker UI.
virtual void checkSpelling(int start=0, int end=-1)=0
Check the spelling.
virtual QString getWord(int pos, int *start=0, int *end=0) const =0
Get the word at the specified cursor position.
virtual void insertWord(int start, int end, const QString &word)=0
Replaces the specified range with the specified word.
static QString decodeLanguageCode(const QString &lang)
Translates a language code to a human readable format (i.e. "en_US" -> "English (United States)").
Definition Checker.cpp:250
QString getLanguage() const
Retreive the current spelling language.
Definition Checker.cpp:132
void addWordToDictionary(const QString &word)
Add the specified word to the user dictionary.
Definition Checker.cpp:196
void ignoreWord(const QString &word) const
Ignore a word for the current session.
Definition Checker.cpp:221
static Codetable * instance()
Get codetable instance.
Definition Codetable.cpp:32
void lookup(const QString &language_code, QString &language_name, QString &country_name, QString &extra) const
Looks up the language and country name for the specified language code. If no matching entries are fo...
Definition Codetable.cpp:38
QtSpell namespace.
Definition Checker.cpp:77
bool checkLanguageInstalled(const QString &lang)
Check whether the dictionary for a language is installed.
Definition Checker.cpp:96