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