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