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