QtSpell  1.0.1
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 
32 static 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 
42 static 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 
52 class TranslationsInit {
53 public:
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  }
72 private:
73  QTranslator spellTranslator;
74 };
75 
76 
77 namespace QtSpell {
78 
79 CheckerPrivate::CheckerPrivate()
80 {
81 }
82 
83 CheckerPrivate::~CheckerPrivate()
84 {
85  delete speller;
86 }
87 
88 void CheckerPrivate::init()
89 {
90  static TranslationsInit tsInit;
91  Q_UNUSED(tsInit);
92 
93  setLanguageInternal("");
94 }
95 
96 bool checkLanguageInstalled(const QString &lang)
97 {
98  return get_enchant_broker()->dict_exists(lang.toStdString());
99 }
100 
101 Checker::Checker(QObject* parent)
102  : QObject(parent)
103  , d_ptr(new CheckerPrivate())
104 {
105  d_ptr->q_ptr = this;
106  d_ptr->init();
107 }
108 
109 Checker::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 
122 bool Checker::setLanguage(const QString &lang)
123 {
124  Q_D(Checker);
125  bool success = d->setLanguageInternal(lang);
126  if(isAttached()){
127  checkSpelling();
128  }
129  return success;
130 }
131 
132 QString Checker::getLanguage() const
133 {
134  Q_D(const Checker);
135  return d->lang;
136 }
137 
138 bool 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 
196 void Checker::addWordToDictionary(const QString &word)
197 {
198  Q_D(Checker);
199  if(d->speller){
200  d->speller->add(word.toUtf8().data());
201  }
202 }
203 
204 bool 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 
221 void Checker::ignoreWord(const QString &word) const
222 {
223  Q_D(const Checker);
224  d->speller->add_to_session(word.toUtf8().data());
225 }
226 
227 QList<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 
241 QList<QString> Checker::getLanguageList()
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 
250 QString 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 
265 void Checker::setSpellingEnabled(bool enabled)
266 {
267  Q_D(Checker);
268  d->spellingEnabled = enabled;
269  checkSpelling();
270 }
271 
272 void 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 
347 void 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 
355 void 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 
363 void 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 
372 void 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