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