]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiSpellchecker.cpp
Add Sam Crawley to credits.
[lyx.git] / src / frontends / qt / 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  * \author Abdelrazak Younes
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "GuiSpellchecker.h"
16 #include "GuiApplication.h"
17
18 #include "qt_helpers.h"
19
20 #include "ui_SpellcheckerUi.h"
21
22 #include "Buffer.h"
23 #include "BufferParams.h"
24 #include "BufferView.h"
25 #include "buffer_funcs.h"
26 #include "Cursor.h"
27 #include "Text.h"
28 #include "CutAndPaste.h"
29 #include "FuncRequest.h"
30 #include "GuiView.h"
31 #include "Language.h"
32 #include "LyX.h"
33 #include "LyXRC.h"
34 #include "lyxfind.h"
35 #include "Paragraph.h"
36 #include "WordLangTuple.h"
37
38 #include "support/debug.h"
39 #include "support/docstring.h"
40 #include "support/docstring_list.h"
41 #include "support/ExceptionMessage.h"
42 #include "support/gettext.h"
43 #include "support/lstrings.h"
44 #include "support/textutils.h"
45
46 #include <QKeyEvent>
47 #include <QListWidgetItem>
48 #include <QMessageBox>
49
50 #include "SpellChecker.h"
51
52 #include "frontends/alert.h"
53
54 using namespace std;
55 using namespace lyx::support;
56
57 namespace lyx {
58 namespace frontend {
59
60
61 struct SpellcheckerWidget::Private
62 {
63         Private(SpellcheckerWidget * parent, DockView * dv, GuiView * gv)
64                 : p(parent), dv_(dv), gv_(gv), incheck_(false), wrap_around_(false) {}
65         /// update from controller
66         void updateSuggestions(docstring_list & words);
67         /// move to next position after current word
68         void forward();
69         /// check text until next misspelled/unknown word
70         void check();
71         /// close the spell checker dialog
72         void hide() const;
73         /// make/restore a selection between from and to
74         void setSelection(DocIterator const & from, DocIterator const & to) const;
75         /// if no selection was checked:
76         /// ask the user if the check should start over
77         bool continueFromBeginning();
78         /// set the given language in language chooser
79         void setLanguage(Language const * lang);
80         /// test and set guard flag
81         bool inCheck() {
82                 if (incheck_)
83                         return true;
84                 incheck_ = true;
85                 return false;
86         }
87         void canCheck() { incheck_ = false; }
88         /// check for wrap around
89         void wrapAround(bool flag) {
90                 wrap_around_ = flag;
91                 if (flag) {
92                         end_ = start_;
93                 }
94         }
95         /// test for existing association with a document buffer
96         /// and test for already active check
97         bool disabled() {
98                 return gv_->documentBufferView() == 0 || inCheck();
99         }
100         /// the cursor position of the buffer view
101         DocIterator const cursor() const;
102         /// status checks
103         bool isCurrentBuffer(DocIterator const & cursor) const;
104         bool isWrapAround(DocIterator const & cursor) const;
105         bool isWrapAround() const { return wrap_around_; }
106         bool atLastPos(DocIterator const & cursor) const;
107         /// validate the cached doc iterators
108         /// The spell checker dialog is not modal.
109         /// The user may change the buffer being checked and break the iterators.
110         void fixPositionsIfBroken();
111         ///
112         Ui::SpellcheckerUi ui;
113         ///
114         SpellcheckerWidget * p;
115         ///
116         DockView * dv_;
117         ///
118         GuiView * gv_;
119         /// current word being checked and lang code
120         WordLangTuple word_;
121         /// cursor position where spell checking starts
122         DocIterator start_;
123         /// range to spell check
124         /// for selection both are non-empty
125         /// after wrap around the start position becomes the end
126         DocIterator begin_;
127         DocIterator end_;
128         ///
129         bool incheck_;
130         ///
131         bool wrap_around_;
132 };
133
134
135 SpellcheckerWidget::SpellcheckerWidget(GuiView * gv, DockView * dv, QWidget * parent)
136         : QTabWidget(parent), d(new Private(this, dv, gv))
137 {
138         d->ui.setupUi(this);
139
140         connect(d->ui.suggestionsLW, SIGNAL(itemDoubleClicked(QListWidgetItem*)),
141                 this, SLOT(on_replacePB_clicked()));
142
143         // language
144         QAbstractItemModel * language_model = guiApp->languageModel();
145         // FIXME: it would be nice if sorting was enabled/disabled via a checkbox.
146         language_model->sort(0);
147         d->ui.languageCO->setModel(language_model);
148         d->ui.languageCO->setModelColumn(1);
149
150         d->ui.wordED->setReadOnly(true);
151
152         d->ui.suggestionsLW->installEventFilter(this);
153 }
154
155
156 SpellcheckerWidget::~SpellcheckerWidget()
157 {
158         delete d;
159 }
160
161
162 bool SpellcheckerWidget::eventFilter(QObject *obj, QEvent *event)
163 {
164         if (obj == d->ui.suggestionsLW && event->type() == QEvent::KeyPress) {
165                 QKeyEvent *e = static_cast<QKeyEvent *> (event);
166                 if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
167                         if (d->ui.suggestionsLW->currentItem()) {
168                                 on_suggestionsLW_itemClicked(d->ui.suggestionsLW->currentItem());
169                                 on_replacePB_clicked();
170                         }
171                         return true;
172                 } else if (e->key() == Qt::Key_Right) {
173                         if (d->ui.suggestionsLW->currentItem())
174                                 on_suggestionsLW_itemClicked(d->ui.suggestionsLW->currentItem());
175                         return true;
176                 }
177         }
178         // standard event processing
179         return QWidget::eventFilter(obj, event);
180 }
181
182
183 void SpellcheckerWidget::on_suggestionsLW_itemClicked(QListWidgetItem * item)
184 {
185         if (d->ui.replaceCO->count() != 0)
186                 d->ui.replaceCO->setItemText(0, item->text());
187         else
188                 d->ui.replaceCO->addItem(item->text());
189
190         d->ui.replaceCO->setCurrentIndex(0);
191 }
192
193
194 void SpellcheckerWidget::on_replaceCO_highlighted(const QString & str)
195 {
196         QListWidget * lw = d->ui.suggestionsLW;
197         if (lw->currentItem() && lw->currentItem()->text() == str)
198                 return;
199
200         for (int i = 0; i != lw->count(); ++i) {
201                 if (lw->item(i)->text() == str) {
202                         lw->setCurrentRow(i);
203                         break;
204                 }
205         }
206 }
207
208
209 void SpellcheckerWidget::updateView()
210 {
211         BufferView * bv = d->gv_->documentBufferView();
212         // we need a buffer view and the buffer has to be writable
213         bool const enabled = bv != 0 && !bv->buffer().isReadonly();
214         setEnabled(enabled);
215         if (enabled && hasFocus()) {
216                 Cursor const & cursor = bv->cursor();
217                 if (d->start_.empty() || !d->isCurrentBuffer(cursor)) {
218                         if (cursor.selection()) {
219                                 d->begin_ = cursor.selectionBegin();
220                                 d->end_   = cursor.selectionEnd();
221                                 d->start_ = d->begin_;
222                                 bv->cursor().setCursor(d->start_);
223                         } else {
224                                 d->begin_ = DocIterator();
225                                 d->end_   = DocIterator();
226                                 d->start_ = cursor;
227                         }
228                         d->wrapAround(false);
229                         d->check();
230                 }
231         }
232 }
233
234 DocIterator const SpellcheckerWidget::Private::cursor() const
235 {
236         BufferView * bv = gv_->documentBufferView();
237         return bv ? bv->cursor() : DocIterator();
238 }
239
240 bool SpellcheckerWidget::Private::continueFromBeginning()
241 {
242         DocIterator const current_ = cursor();
243         if (isCurrentBuffer(current_) && !begin_.empty()) {
244                 // selection was checked
245                 // start over from beginning makes no sense
246                 fixPositionsIfBroken();
247                 hide();
248                 if (current_ == start_) {
249                         // no errors found... tell the user the good news
250                         // so there is some feedback
251                         QMessageBox::information(p,
252                                 qt_("Spell Checker"),
253                                 qt_("Spell check of the selection done, "
254                                         "did not find any errors."));
255                 }
256                 return false;
257         }
258         QMessageBox::StandardButton const answer = QMessageBox::question(p,
259                 qt_("Spell Checker"),
260                 qt_("We reached the end of the document, would you like to "
261                         "continue from the beginning?"),
262                 QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
263         if (answer == QMessageBox::No) {
264                 fixPositionsIfBroken();
265                 hide();
266                 return false;
267         }
268         // there is no selection, start over from the beginning now
269         wrapAround(true);
270         dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
271         return true;
272 }
273
274 bool SpellcheckerWidget::Private::isCurrentBuffer(DocIterator const & cursor) const
275 {
276         return start_.buffer() == cursor.buffer();
277 }
278
279 bool SpellcheckerWidget::Private::atLastPos(DocIterator const & cursor) const
280 {
281         bool const valid_end = !end_.empty();
282         return cursor.depth() <= 1 && (
283                 cursor.atEnd() ||
284                 (valid_end && isCurrentBuffer(cursor) && cursor >= end_));
285 }
286
287 bool SpellcheckerWidget::Private::isWrapAround(DocIterator const & cursor) const
288 {
289         return wrap_around_ && isCurrentBuffer(cursor) && start_ < cursor;
290 }
291
292 void SpellcheckerWidget::Private::fixPositionsIfBroken()
293 {
294         DocIterator const current_ = cursor();
295         if (!isCurrentBuffer(current_)) {
296                 LYXERR(Debug::GUI, "wrong document of current cursor position " << start_);
297                 start_ = current_;
298                 begin_ = DocIterator();
299                 end_   = DocIterator();
300         }
301         if (start_.fixIfBroken())
302                 LYXERR(Debug::GUI, "broken start position fixed " << start_);
303         if (begin_.fixIfBroken()) {
304                 LYXERR(Debug::GUI, "broken selection begin position fixed " << begin_);
305                 begin_ = DocIterator();
306                 end_   = DocIterator();
307         }
308         if (end_.fixIfBroken())
309                 LYXERR(Debug::GUI, "broken selection end position fixed " << end_);
310 }
311
312 void SpellcheckerWidget::Private::hide() const
313 {
314         BufferView * bv = gv_->documentBufferView();
315         Cursor & bvcur = bv->cursor();
316         dv_->hide();
317         if (isCurrentBuffer(bvcur)) {
318                 if (!begin_.empty() && !end_.empty()) {
319                         // restore previous selection
320                         setSelection(begin_, end_);
321                 } else {
322                         // restore cursor position
323                         bvcur.setCursor(start_);
324                         bvcur.clearSelection();
325                         bv->processUpdateFlags(Update::Force | Update::FitCursor);
326                 }
327         }
328 }
329
330 void SpellcheckerWidget::Private::setSelection(
331         DocIterator const & from, DocIterator const & to) const
332 {
333         BufferView * bv = gv_->documentBufferView();
334         DocIterator end = to;
335
336         if (from.pit() != end.pit()) {
337                 // there are multiple paragraphs in selection
338                 Cursor & bvcur = bv->cursor();
339                 bvcur.setCursor(from);
340                 bvcur.clearSelection();
341                 bvcur.selection(true);
342                 bvcur.setCursor(end);
343                 bvcur.selection(true);
344         } else {
345                 // FIXME LFUN
346                 // If we used a LFUN, dispatch would do all of this for us
347                 int const size = end.pos() - from.pos();
348                 bv->putSelectionAt(from, size, false);
349         }
350         bv->processUpdateFlags(Update::Force | Update::FitCursor);
351 }
352
353 void SpellcheckerWidget::Private::forward()
354 {
355         DocIterator const from = cursor();
356
357         dispatch(FuncRequest(LFUN_ESCAPE));
358         fixPositionsIfBroken();
359         if (!atLastPos(cursor())) {
360                 dispatch(FuncRequest(LFUN_CHAR_FORWARD));
361         }
362         if (atLastPos(cursor())) {
363                 return;
364         }
365         if (from == cursor()) {
366                 //FIXME we must be at the end of a cell
367                 dispatch(FuncRequest(LFUN_CHAR_FORWARD));
368         }
369         if (isWrapAround(cursor())) {
370                 hide();
371         }
372 }
373
374
375 void SpellcheckerWidget::on_languageCO_activated(int index)
376 {
377         string const lang =
378                 fromqstr(d->ui.languageCO->itemData(index).toString());
379         if (!d->word_.lang() || d->word_.lang()->lang() == lang)
380                 // nothing changed
381                 return;
382         dispatch(FuncRequest(LFUN_LANGUAGE, lang));
383         d->check();
384 }
385
386
387 bool SpellcheckerWidget::initialiseParams(std::string const &)
388 {
389         BufferView * bv = d->gv_->documentBufferView();
390         if (bv == 0)
391                 return false;
392         std::set<Language const *> langs =
393                 bv->buffer().masterBuffer()->getLanguages();
394         if (!langs.empty())
395                 d->setLanguage(*langs.begin());
396         d->start_ = DocIterator();
397         d->wrapAround(false);
398         d->canCheck();
399         return true;
400 }
401
402
403 void SpellcheckerWidget::on_ignoreAllPB_clicked()
404 {
405         /// ignore all occurrences of word
406         if (d->disabled())
407                 return;
408         LYXERR(Debug::GUI, "Spellchecker: ignore all button");
409         if (d->word_.lang() && !d->word_.word().empty())
410                 theSpellChecker()->accept(d->word_);
411         d->forward();
412         d->check();
413         d->canCheck();
414 }
415
416
417 void SpellcheckerWidget::on_addPB_clicked()
418 {
419         /// insert word in personal dictionary
420         if (d->disabled())
421                 return;
422         LYXERR(Debug::GUI, "Spellchecker: add word button");
423         theSpellChecker()->insert(d->word_);
424         d->forward();
425         d->check();
426         d->canCheck();
427 }
428
429
430 void SpellcheckerWidget::on_ignorePB_clicked()
431 {
432         /// ignore this occurrence of word
433         if (d->disabled())
434                 return;
435         LYXERR(Debug::GUI, "Spellchecker: ignore button");
436         d->forward();
437         d->check();
438         d->canCheck();
439 }
440
441
442 void SpellcheckerWidget::on_findNextPB_clicked()
443 {
444         if (d->disabled())
445                 return;
446         docstring const textfield = qstring_to_ucs4(d->ui.wordED->text());
447         docstring const datastring = find2string(textfield,
448                                 true, true, true);
449         LYXERR(Debug::GUI, "Spellchecker: find next (" << textfield << ")");
450         dispatch(FuncRequest(LFUN_WORD_FIND, datastring));
451         d->canCheck();
452 }
453
454
455 void SpellcheckerWidget::on_replacePB_clicked()
456 {
457         if (d->disabled())
458                 return;
459         docstring const textfield = qstring_to_ucs4(d->ui.wordED->text());
460         docstring const replacement = qstring_to_ucs4(d->ui.replaceCO->currentText());
461         docstring const datastring =
462                 replace2string(replacement, textfield,
463                         true,   // case sensitive
464                         true,   // match word
465                         false,  // all words
466                         true,   // forward
467                         false); // find next
468
469         LYXERR(Debug::GUI, "Replace (" << replacement << ")");
470         dispatch(FuncRequest(LFUN_WORD_REPLACE, datastring));
471         d->forward();
472         d->check();
473         d->canCheck();
474 }
475
476
477 void SpellcheckerWidget::on_replaceAllPB_clicked()
478 {
479         if (d->disabled())
480                 return;
481         docstring const textfield = qstring_to_ucs4(d->ui.wordED->text());
482         docstring const replacement = qstring_to_ucs4(d->ui.replaceCO->currentText());
483         docstring const datastring =
484                 replace2string(replacement, textfield,
485                         true,   // case sensitive
486                         true,   // match word
487                         true,   // all words
488                         true,   // forward
489                         false); // find next
490
491         LYXERR(Debug::GUI, "Replace all (" << replacement << ")");
492         dispatch(FuncRequest(LFUN_WORD_REPLACE, datastring));
493         d->forward();
494         // replace all wraps around
495         d->wrapAround(true);
496         d->check(); // continue spellchecking
497         d->canCheck();
498 }
499
500
501 void SpellcheckerWidget::Private::updateSuggestions(docstring_list & words)
502 {
503         QString const suggestion = toqstr(word_.word());
504         ui.wordED->setText(suggestion);
505         QListWidget * lw = ui.suggestionsLW;
506         lw->clear();
507
508         if (words.empty()) {
509                 p->on_suggestionsLW_itemClicked(new QListWidgetItem(suggestion));
510                 return;
511         }
512         for (size_t i = 0; i != words.size(); ++i)
513                 lw->addItem(toqstr(words[i]));
514
515         p->on_suggestionsLW_itemClicked(lw->item(0));
516         lw->setCurrentRow(0);
517 }
518
519
520 void SpellcheckerWidget::Private::setLanguage(Language const * lang)
521 {
522         int const pos = ui.languageCO->findData(toqstr(lang->lang()));
523         if (pos != -1)
524                 ui.languageCO->setCurrentIndex(pos);
525 }
526
527
528 void SpellcheckerWidget::Private::check()
529 {
530         BufferView * bv = gv_->documentBufferView();
531         if (!bv || bv->buffer().text().empty())
532                 return;
533
534         fixPositionsIfBroken();
535
536         SpellChecker * speller = theSpellChecker();
537         if (speller && !speller->hasDictionary(bv->buffer().language())) {
538                 int dsize = speller->numDictionaries();
539                 if (0 == dsize) {
540                         hide();
541                         QMessageBox::information(p,
542                                 qt_("Spell Checker"),
543                                 qt_("Spell checker has no dictionaries."));
544                         return;
545                 }
546         }
547
548         DocIterator from = bv->cursor();
549         DocIterator to = isCurrentBuffer(from) ? end_ : doc_iterator_end(&bv->buffer());
550         WordLangTuple word_lang;
551         docstring_list suggestions;
552
553         LYXERR(Debug::GUI, "Spellchecker: start check at " << from);
554         try {
555                 bv->buffer().spellCheck(from, to, word_lang, suggestions);
556         } catch (ExceptionMessage const & message) {
557                 if (message.type_ == WarningException) {
558                         Alert::warning(message.title_, message.details_);
559                         return;
560                 }
561                 throw;
562         }
563
564         // end of document or selection?
565         if (atLastPos(from)) {
566                 if (isWrapAround()) {
567                         hide();
568                         return;
569                 }
570                 if (continueFromBeginning())
571                         check();
572                 return;
573         }
574
575         if (isWrapAround(from)) {
576                 hide();
577                 return;
578         }
579
580         word_ = word_lang;
581
582         // set suggestions
583         updateSuggestions(suggestions);
584         // set language
585         if (!word_lang.lang())
586                 return;
587         setLanguage(word_lang.lang());
588         // mark misspelled word
589         setSelection(from, to);
590 }
591
592
593 GuiSpellchecker::GuiSpellchecker(GuiView & parent,
594                 Qt::DockWidgetArea area, Qt::WindowFlags flags)
595         : DockView(parent, "spellchecker", qt_("Spellchecker"),
596                    area, flags)
597 {
598         widget_ = new SpellcheckerWidget(&parent, this);
599         setWidget(widget_);
600         setFocusProxy(widget_);
601 }
602
603
604 GuiSpellchecker::~GuiSpellchecker()
605 {
606         setFocusProxy(0);
607         delete widget_;
608 }
609
610
611 void GuiSpellchecker::updateView()
612 {
613         widget_->updateView();
614 }
615
616
617 Dialog * createGuiSpellchecker(GuiView & lv)
618 {
619         GuiSpellchecker * gui = new GuiSpellchecker(lv, Qt::RightDockWidgetArea);
620 #ifdef Q_OS_MAC
621         gui->setFloating(true);
622 #endif
623         return gui;
624 }
625
626
627 } // namespace frontend
628 } // namespace lyx
629
630 #include "moc_GuiSpellchecker.cpp"