]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiSpellchecker.cpp
remove ispell and pspell
[lyx.git] / src / frontends / qt4 / GuiSpellchecker.cpp
1 /**
2  * \file GuiSpellchecker.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author John Levon
7  * \author Edwin Leuven
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "GuiSpellchecker.h"
15
16 #include "qt_helpers.h"
17
18 #include "Buffer.h"
19 #include "BufferParams.h"
20 #include "BufferView.h"
21 #include "Cursor.h"
22 #include "CutAndPaste.h"
23 #include "Language.h"
24 #include "LyXRC.h"
25 #include "Paragraph.h"
26
27 #include "support/debug.h"
28 #include "support/docstring.h"
29 #include "support/gettext.h"
30 #include "support/lstrings.h"
31 #include "support/textutils.h"
32
33 #include <QListWidgetItem>
34
35 #if defined(USE_ASPELL)
36 # include "ASpell_local.h"
37 #endif
38
39 #include "SpellBase.h"
40
41 #include "frontends/alert.h"
42
43 using namespace std;
44 using namespace lyx::support;
45
46 namespace lyx {
47 namespace frontend {
48
49
50 GuiSpellchecker::GuiSpellchecker(GuiView & lv)
51         : GuiDialog(lv, "spellchecker", qt_("Spellchecker")), exitEarly_(false),
52           oldval_(0), newvalue_(0), count_(0), speller_(0)
53 {
54         setupUi(this);
55
56         connect(closePB, SIGNAL(clicked()), this, SLOT(slotClose()));
57         connect(replacePB, SIGNAL(clicked()), this, SLOT(replace()));
58         connect(ignorePB, SIGNAL(clicked()), this, SLOT(ignore()));
59         connect(replacePB_3, SIGNAL(clicked()), this, SLOT(accept()));
60         connect(addPB, SIGNAL(clicked()), this, SLOT(add()));
61
62         connect(replaceCO, SIGNAL(highlighted(QString)),
63                 this, SLOT(replaceChanged(QString)));
64         connect(suggestionsLW, SIGNAL(itemDoubleClicked(QListWidgetItem*)),
65                 this, SLOT(replace()));
66         connect(suggestionsLW, SIGNAL(itemClicked(QListWidgetItem*)),
67                 this, SLOT(suggestionChanged(QListWidgetItem*)));
68
69         wordED->setReadOnly(true);
70
71         bc().setPolicy(ButtonPolicy::NoRepeatedApplyReadOnlyPolicy);
72         bc().setCancel(closePB);
73 }
74
75
76 GuiSpellchecker::~GuiSpellchecker()
77 {
78         delete speller_;
79 }
80
81
82 void GuiSpellchecker::suggestionChanged(QListWidgetItem * item)
83 {
84         if (replaceCO->count() != 0)
85                 replaceCO->setItemText(0, item->text());
86         else
87                 replaceCO->addItem(item->text());
88
89         replaceCO->setCurrentIndex(0);
90 }
91
92
93 void GuiSpellchecker::replaceChanged(const QString & str)
94 {
95         if (suggestionsLW->currentItem()->text() == str)
96                 return;
97
98         for (int i = 0; i != suggestionsLW->count(); ++i) {
99                 if (suggestionsLW->item(i)->text() == str) {
100                         suggestionsLW->setCurrentRow(i);
101                         break;
102                 }
103         }
104 }
105
106
107 void GuiSpellchecker::reject()
108 {
109         slotClose();
110         QDialog::reject();
111 }
112
113
114 void GuiSpellchecker::updateContents()
115 {
116         // The clauses below are needed because the spellchecker
117         // has many flaws (see bugs 1950, 2218).
118         // Basically, we have to distinguish the case where a
119         // spellcheck has already been performed for the whole
120         // document (exitEarly() == true, isVisible() == false) 
121         // from the rest (exitEarly() == false, isVisible() == true).
122         // FIXME: rewrite the whole beast!
123         static bool check_after_early_exit;
124         if (exitEarly()) {
125                 // a spellcheck has already been performed,
126                 check();
127                 check_after_early_exit = true;
128         }
129         else if (isVisible()) {
130                 // the above check triggers a second update,
131                 // and isVisible() is true then. Prevent a
132                 // second check that skips the first word
133                 if (check_after_early_exit)
134                         // don't check, but reset the bool.
135                         // business as usual after this.
136                         check_after_early_exit = false;
137                 else
138                         // perform spellcheck (default case)
139                         check();
140         }
141 }
142
143
144 void GuiSpellchecker::accept()
145 {
146         ignoreAll();
147 }
148
149
150 void GuiSpellchecker::add()
151 {
152         insert();
153 }
154
155
156 void GuiSpellchecker::ignore()
157 {
158         check();
159 }
160
161
162 void GuiSpellchecker::replace()
163 {
164         replace(qstring_to_ucs4(replaceCO->currentText()));
165 }
166
167
168 void GuiSpellchecker::partialUpdate(int state)
169 {
170         switch (state) {
171                 case SPELL_PROGRESSED:
172                         spellcheckPR->setValue(getProgress());
173                         break;
174
175                 case SPELL_FOUND_WORD: {
176                         wordED->setText(toqstr(getWord()));
177                         suggestionsLW->clear();
178
179                         docstring w;
180                         while (!(w = getSuggestion()).empty())
181                                 suggestionsLW->addItem(toqstr(w));
182
183                         if (suggestionsLW->count() == 0)
184                                 suggestionChanged(new QListWidgetItem(wordED->text()));
185                         else
186                                 suggestionChanged(suggestionsLW->item(0));
187
188                         suggestionsLW->setCurrentRow(0);
189                         break;
190                 }
191         }
192 }
193
194
195 static SpellBase * createSpeller(BufferParams const & bp)
196 {
197         string lang = lyxrc.spellchecker_use_alt_lang
198                       ? lyxrc.spellchecker_alt_lang
199                       : bp.language->code();
200
201 #if defined(USE_ASPELL)
202         if (lyxrc.use_spell_lib)
203                 return new ASpell(bp, lang);
204 #endif
205         return new SpellBase;
206 }
207
208
209 bool GuiSpellchecker::initialiseParams(string const &)
210 {
211         LYXERR(Debug::GUI, "Spellchecker::initialiseParams");
212
213         speller_ = createSpeller(buffer().params());
214         if (!speller_)
215                 return false;
216
217         // reset values to initial
218         oldval_ = 0;
219         newvalue_ = 0;
220         count_ = 0;
221
222         bool const success = speller_->error().empty();
223
224         if (!success) {
225                 Alert::error(_("Spellchecker error"),
226                              _("The spellchecker could not be started\n")
227                              + speller_->error());
228                 delete speller_;
229                 speller_ = 0;
230         }
231
232         return success;
233 }
234
235
236 void GuiSpellchecker::clearParams()
237 {
238         LYXERR(Debug::GUI, "Spellchecker::clearParams");
239         delete speller_;
240         speller_ = 0;
241 }
242
243
244 static bool isLetter(DocIterator const & dit)
245 {
246         return dit.inTexted()
247                 && dit.inset().allowSpellCheck()
248                 && dit.pos() != dit.lastpos()
249                 && (dit.paragraph().isLetter(dit.pos())
250                     // We want to pass the ' and escape chars to ispell
251                     || contains(from_utf8(lyxrc.spellchecker_esc_chars + '\''),
252                                 dit.paragraph().getChar(dit.pos())))
253                 && !dit.paragraph().isDeleted(dit.pos());
254 }
255
256
257 static WordLangTuple nextWord(Cursor & cur, ptrdiff_t & progress)
258 {
259         BufferParams const & bp = cur.bv().buffer().params();
260         bool inword = false;
261         bool ignoreword = false;
262         cur.resetAnchor();
263         docstring word;
264         string lang_code;
265
266         while (cur.depth()) {
267                 if (isLetter(cur)) {
268                         if (!inword) {
269                                 inword = true;
270                                 ignoreword = false;
271                                 cur.resetAnchor();
272                                 word.clear();
273                                 lang_code = cur.paragraph().getFontSettings(bp, cur.pos()).language()->code();
274                         }
275                         // Insets like optional hyphens and ligature
276                         // break are part of a word.
277                         if (!cur.paragraph().isInset(cur.pos())) {
278                                 char_type const c = cur.paragraph().getChar(cur.pos());
279                                 word += c;
280                                 if (isDigit(c))
281                                         ignoreword = true;
282                         }
283                 } else { // !isLetter(cur)
284                         if (inword)
285                                 if (!word.empty() && !ignoreword) {
286                                         cur.setSelection();
287                                         return WordLangTuple(word, lang_code);
288                                 }
289                                 inword = false;
290                 }
291
292                 cur.forwardPos();
293                 ++progress;
294         }
295
296         return WordLangTuple(docstring(), string());
297 }
298
299
300 void GuiSpellchecker::check()
301 {
302         LYXERR(Debug::GUI, "Check the spelling of a word");
303
304         SpellBase::Result res = SpellBase::OK;
305
306         Cursor cur = bufferview()->cursor();
307         while (cur && cur.pos() && isLetter(cur))
308                 cur.backwardPos();
309
310         ptrdiff_t start = 0;
311         ptrdiff_t total = 0;
312         DocIterator it = doc_iterator_begin(buffer().inset());
313         for (start = 1; it != cur; it.forwardPos())
314                 ++start;
315
316         for (total = start; it; it.forwardPos())
317                 ++total;
318
319         exitEarly_ = false;
320
321         while (res == SpellBase::OK || res == SpellBase::IGNORED_WORD) {
322                 word_ = nextWord(cur, start);
323
324                 // end of document
325                 if (getWord().empty()) {
326                         showSummary();
327                         exitEarly_ = true;
328                         return;
329                 }
330
331                 ++count_;
332
333                 // Update slider if and only if value has changed
334                 float progress = total ? float(start)/total : 1;
335                 newvalue_ = int(100.0 * progress);
336                 if (newvalue_!= oldval_) {
337                         LYXERR(Debug::GUI, "Updating spell progress.");
338                         oldval_ = newvalue_;
339                         // set progress bar
340                         partialUpdate(SPELL_PROGRESSED);
341                 }
342
343                 // speller might be dead ...
344                 if (!checkAlive())
345                         return;
346
347                 res = speller_->check(word_);
348
349                 // ... or it might just be reporting an error
350                 if (!checkAlive())
351                         return;
352         }
353
354         LYXERR(Debug::GUI, "Found word \"" << to_utf8(getWord()) << "\"");
355
356         int const size = cur.selEnd().pos() - cur.selBegin().pos();
357         cur.pos() -= size;
358         BufferView * bv = const_cast<BufferView *>(bufferview());
359         bv->putSelectionAt(cur, size, false);
360         // FIXME: if we used a lfun like in find/replace, dispatch would do
361         // that for us
362         // FIXME: this Controller is very badly designed...
363         bv->processUpdateFlags(Update::Force | Update::FitCursor);
364
365         // set suggestions
366         if (res != SpellBase::OK && res != SpellBase::IGNORED_WORD) {
367                 LYXERR(Debug::GUI, "Found a word needing checking.");
368                 partialUpdate(SPELL_FOUND_WORD);
369         }
370 }
371
372
373 bool GuiSpellchecker::checkAlive()
374 {
375         if (speller_->alive() && speller_->error().empty())
376                 return true;
377
378         docstring message;
379         if (speller_->error().empty())
380                 message = _("The spellchecker has died for some reason.\n"
381                                          "Maybe it has been killed.");
382         else
383                 message = _("The spellchecker has failed.\n") + speller_->error();
384
385         slotClose();
386
387         Alert::error(_("The spellchecker has failed"), message);
388         return false;
389 }
390
391
392 void GuiSpellchecker::showSummary()
393 {
394         if (!checkAlive() || count_ == 0) {
395                 slotClose();
396                 return;
397         }
398
399         docstring message;
400         if (count_ != 1)
401                 message = bformat(_("%1$d words checked."), count_);
402         else
403                 message = _("One word checked.");
404
405         slotClose();
406         Alert::information(_("Spelling check completed"), message);
407 }
408
409
410 void GuiSpellchecker::replace(docstring const & replacement)
411 {
412         LYXERR(Debug::GUI, "GuiSpellchecker::replace("
413                            << to_utf8(replacement) << ")");
414         BufferView * bv = const_cast<BufferView *>(bufferview());
415         cap::replaceSelectionWithString(bv->cursor(), replacement, true);
416         bv->buffer().markDirty();
417         // If we used an LFUN, we would not need that
418         bv->processUpdateFlags(Update::Force | Update::FitCursor);
419         // fix up the count
420         --count_;
421         check();
422 }
423
424
425 void GuiSpellchecker::replaceAll(docstring const & replacement)
426 {
427         // TODO: add to list
428         replace(replacement);
429 }
430
431
432 void GuiSpellchecker::insert()
433 {
434         speller_->insert(word_);
435         check();
436 }
437
438
439 docstring GuiSpellchecker::getSuggestion() const
440 {
441         return speller_->nextMiss();
442 }
443
444
445 docstring GuiSpellchecker::getWord() const
446 {
447         return word_.word();
448 }
449
450
451 void GuiSpellchecker::ignoreAll()
452 {
453         speller_->accept(word_);
454         check();
455 }
456
457
458 Dialog * createGuiSpellchecker(GuiView & lv) { return new GuiSpellchecker(lv); }
459
460 } // namespace frontend
461 } // namespace lyx
462
463 #include "moc_GuiSpellchecker.cpp"