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