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