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