QtSpell  0.9.0
Spell checking for Qt text widgets
Checker.cpp
1 /* QtSpell - Spell checking for Qt text widgets.
2  * Copyright (c) 2014 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 "Codetable.hpp"
21 
22 #include <enchant++.h>
23 #include <QApplication>
24 #include <QLibraryInfo>
25 #include <QLocale>
26 #include <QMenu>
27 #include <QTranslator>
28 #include <QtDebug>
29 
30 static void dict_describe_cb(const char* const lang_tag,
31  const char* const /*provider_name*/,
32  const char* const /*provider_desc*/,
33  const char* const /*provider_file*/,
34  void* user_data)
35 {
36  QList<QString>* languages = static_cast<QList<QString>*>(user_data);
37  languages->append(lang_tag);
38 }
39 
40 static enchant::Broker* get_enchant_broker() {
41 #ifdef QTSPELL_ENCHANT2
42  static enchant::Broker broker;
43  return &broker;
44 #else
45  return enchant::Broker::instance();
46 #endif
47 }
48 
49 
50 class TranslationsInit {
51 public:
52  TranslationsInit(){
53  QString translationsPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
54 #ifdef Q_OS_WIN
55  QDir packageDir = QDir(QString("%1/../").arg(QApplication::applicationDirPath()));
56  translationsPath = packageDir.absolutePath() + translationsPath.mid(QLibraryInfo::location(QLibraryInfo::PrefixPath).length());
57 #endif
58  spellTranslator.load("QtSpell_" + QLocale::system().name(), translationsPath);
59  QApplication::instance()->installTranslator(&spellTranslator);
60  }
61 private:
62  QTranslator spellTranslator;
63 };
64 
65 
66 namespace QtSpell {
67 
68 bool checkLanguageInstalled(const QString &lang)
69 {
70  return get_enchant_broker()->dict_exists(lang.toStdString());
71 }
72 
73 Checker::Checker(QObject* parent)
74  : QObject(parent)
75 {
76  static TranslationsInit tsInit;
77  Q_UNUSED(tsInit);
78 
79  // setLanguageInternal: setLanguage is virtual and cannot be called in the constructor
80  setLanguageInternal("");
81 }
82 
84 {
85  delete m_speller;
86 }
87 
88 bool Checker::setLanguage(const QString &lang)
89 {
90  bool success = setLanguageInternal(lang);
91  if(isAttached()){
92  checkSpelling();
93  }
94  return success;
95 }
96 
97 bool Checker::setLanguageInternal(const QString &lang)
98 {
99  delete m_speller;
100  m_speller = nullptr;
101  m_lang = lang;
102 
103  // Determine language from system locale
104  if(m_lang.isEmpty()){
105  m_lang = QLocale::system().name();
106  if(m_lang.toLower() == "c" || m_lang.isEmpty()){
107  qWarning() << "Cannot use system locale " << m_lang;
108  m_lang = QString();
109  return false;
110  }
111  }
112 
113  // Request dictionary
114  try {
115  m_speller = get_enchant_broker()->request_dict(m_lang.toStdString());
116  } catch(enchant::Exception& e) {
117  qWarning() << "Failed to load dictionary: " << e.what();
118  m_lang = QString();
119  return false;
120  }
121 
122  return true;
123 }
124 
125 void Checker::addWordToDictionary(const QString &word)
126 {
127  if(m_speller){
128  m_speller->add(word.toUtf8().data());
129  }
130 }
131 
132 bool Checker::checkWord(const QString &word) const
133 {
134  if(!m_speller || !m_spellingEnabled){
135  return true;
136  }
137  // Skip empty strings and single characters
138  if(word.length() < 2){
139  return true;
140  }
141  try{
142  return m_speller->check(word.toUtf8().data());
143  }catch(const enchant::Exception&){
144  return true;
145  }
146 }
147 
148 void Checker::ignoreWord(const QString &word) const
149 {
150  m_speller->add_to_session(word.toUtf8().data());
151 }
152 
153 QList<QString> Checker::getSpellingSuggestions(const QString& word) const
154 {
155  QList<QString> list;
156  if(m_speller){
157  std::vector<std::string> suggestions;
158  m_speller->suggest(word.toUtf8().data(), suggestions);
159  for(std::size_t i = 0, n = suggestions.size(); i < n; ++i){
160  list.append(QString::fromUtf8(suggestions[i].c_str()));
161  }
162  }
163  return list;
164 }
165 
166 QList<QString> Checker::getLanguageList()
167 {
168  enchant::Broker* broker = get_enchant_broker();
169  QList<QString> languages;
170  broker->list_dicts(dict_describe_cb, &languages);
171  std::sort(languages.begin(), languages.end());
172  return languages;
173 }
174 
175 QString Checker::decodeLanguageCode(const QString &lang)
176 {
177  QString language, country, extra;
178  Codetable::instance()->lookup(lang, language, country, extra);
179  if(!country.isEmpty()){
180  QString decoded = QString("%1 (%2)").arg(language, country);
181  if(!extra.isEmpty()) {
182  decoded += QString(" [%1]").arg(extra);
183  }
184  return decoded;
185  }else{
186  return language;
187  }
188 }
189 
190 void Checker::showContextMenu(QMenu* menu, const QPoint& pos, int wordPos)
191 {
192  QAction* insertPos = menu->actions().first();
193  if(m_speller && m_spellingEnabled){
194  QString word = getWord(wordPos);
195 
196  if(!checkWord(word)) {
197  QList<QString> suggestions = getSpellingSuggestions(word);
198  if(!suggestions.isEmpty()){
199  for(int i = 0, n = qMin(10, suggestions.length()); i < n; ++i){
200  QAction* action = new QAction(suggestions[i], menu);
201  action->setProperty("wordPos", wordPos);
202  action->setProperty("suggestion", suggestions[i]);
203  connect(action, &QAction::triggered, this, &Checker::slotReplaceWord);
204  menu->insertAction(insertPos, action);
205  }
206  if(suggestions.length() > 10) {
207  QMenu* moreMenu = new QMenu();
208  for(int i = 10, n = suggestions.length(); i < n; ++i){
209  QAction* action = new QAction(suggestions[i], moreMenu);
210  action->setProperty("wordPos", wordPos);
211  action->setProperty("suggestion", suggestions[i]);
212  connect(action, &QAction::triggered, this, &Checker::slotReplaceWord);
213  moreMenu->addAction(action);
214  }
215  QAction* action = new QAction(tr("More..."), menu);
216  menu->insertAction(insertPos, action);
217  action->setMenu(moreMenu);
218  }
219  menu->insertSeparator(insertPos);
220  }
221 
222  QAction* addAction = new QAction(tr("Add \"%1\" to dictionary").arg(word), menu);
223  addAction->setData(wordPos);
224  connect(addAction, &QAction::triggered, this, &Checker::slotAddWord);
225  menu->insertAction(insertPos, addAction);
226 
227  QAction* ignoreAction = new QAction(tr("Ignore \"%1\"").arg(word), menu);
228  ignoreAction->setData(wordPos);
229  connect(ignoreAction, &QAction::triggered, this, &Checker::slotIgnoreWord);
230  menu->insertAction(insertPos, ignoreAction);
231  menu->insertSeparator(insertPos);
232  }
233  }
234  if(m_spellingCheckbox){
235  QAction* action = new QAction(tr("Check spelling"), menu);
236  action->setCheckable(true);
237  action->setChecked(m_spellingEnabled);
238  connect(action, &QAction::toggled, this, &Checker::setSpellingEnabled);
239  menu->insertAction(insertPos, action);
240  }
241  if(m_speller && m_spellingEnabled){
242  QMenu* languagesMenu = new QMenu();
243  QActionGroup* actionGroup = new QActionGroup(languagesMenu);
244  foreach(const QString& lang, getLanguageList()){
245  QString text = getDecodeLanguageCodes() ? decodeLanguageCode(lang) : lang;
246  QAction* action = new QAction(text, languagesMenu);
247  action->setData(lang);
248  action->setCheckable(true);
249  action->setChecked(lang == getLanguage());
250  connect(action, &QAction::triggered, this, &Checker::slotSetLanguage);
251  languagesMenu->addAction(action);
252  actionGroup->addAction(action);
253  }
254  QAction* langsAction = new QAction(tr("Languages"), menu);
255  langsAction->setMenu(languagesMenu);
256  menu->insertAction(insertPos, langsAction);
257  menu->insertSeparator(insertPos);
258  }
259 
260  menu->exec(pos);
261  delete menu;
262 }
263 
264 void Checker::slotAddWord()
265 {
266  int wordPos = qobject_cast<QAction*>(QObject::sender())->data().toInt();
267  int start, end;
268  addWordToDictionary(getWord(wordPos, &start, &end));
269  checkSpelling(start, end);
270 }
271 
272 void Checker::slotIgnoreWord()
273 {
274  int wordPos = qobject_cast<QAction*>(QObject::sender())->data().toInt();
275  int start, end;
276  ignoreWord(getWord(wordPos, &start, &end));
277  checkSpelling(start, end);
278 }
279 
280 void Checker::slotReplaceWord()
281 {
282  QAction* action = qobject_cast<QAction*>(QObject::sender());
283  int wordPos = action->property("wordPos").toInt();
284  int start, end;
285  getWord(wordPos, &start, &end);
286  insertWord(start, end, action->property("suggestion").toString());
287 }
288 
289 void Checker::slotSetLanguage(bool checked)
290 {
291  if(checked) {
292  QAction* action = qobject_cast<QAction*>(QObject::sender());
293  QString lang = action->data().toString();
294  if(!setLanguage(lang)){
295  action->setChecked(false);
296  lang = "";
297  }
298  emit languageChanged(lang);
299  }
300 }
301 
302 } // QtSpell
QtSpell::Checker::setLanguage
bool setLanguage(const QString &lang)
Set the spell checking language.
Definition: Checker.cpp:88
QtSpell::Checker::isAttached
virtual bool isAttached() const =0
Returns whether a widget is attached to the checker.
QtSpell::Checker::ignoreWord
void ignoreWord(const QString &word) const
Ignore a word for the current session.
Definition: Checker.cpp:148
QtSpell::checkLanguageInstalled
bool checkLanguageInstalled(const QString &lang)
Check whether the dictionary for a language is installed.
Definition: Checker.cpp:68
QtSpell::Checker::getSpellingSuggestions
QList< QString > getSpellingSuggestions(const QString &word) const
Retreive a list of spelling suggestions for the misspelled word.
Definition: Checker.cpp:153
QtSpell::Checker::insertWord
virtual void insertWord(int start, int end, const QString &word)=0
Replaces the specified range with the specified word.
QtSpell
QtSpell namespace.
Definition: Checker.cpp:66
QtSpell::Checker::getLanguageList
static QList< QString > getLanguageList()
Requests the list of languages available for spell checking.
Definition: Checker.cpp:166
QtSpell::Checker::setSpellingEnabled
void setSpellingEnabled(bool enabled)
Set whether spell checking should be performed.
Definition: QtSpell.hpp:171
QtSpell::Checker::checkWord
bool checkWord(const QString &word) const
Check the specified word.
Definition: Checker.cpp:132
QtSpell::Checker::getLanguage
const QString & getLanguage() const
Retreive the current spelling language.
Definition: QtSpell.hpp:90
QtSpell::Checker::languageChanged
void languageChanged(const QString &newLang)
This signal is emitted when the user selects a new language from the spellchecker UI.
QtSpell::Checker::Checker
Checker(QObject *parent=0)
QtSpell::Checker object constructor.
Definition: Checker.cpp:73
QtSpell::Checker::getWord
virtual QString getWord(int pos, int *start=0, int *end=0) const =0
Get the word at the specified cursor position.
QtSpell::Checker::getDecodeLanguageCodes
bool getDecodeLanguageCodes() const
Return whether langauge codes are decoded in the UI.
Definition: QtSpell.hpp:103
QtSpell::Codetable::lookup
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::Checker::decodeLanguageCode
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:175
QtSpell::Codetable::instance
static Codetable * instance()
Get codetable instance.
Definition: Codetable.cpp:32
QtSpell::Checker::~Checker
virtual ~Checker()
QtSpell::Checker object destructor.
Definition: Checker.cpp:83
QtSpell::Checker::checkSpelling
virtual void checkSpelling(int start=0, int end=-1)=0
Check the spelling.
QtSpell::Checker::addWordToDictionary
void addWordToDictionary(const QString &word)
Add the specified word to the user dictionary.
Definition: Checker.cpp:125