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