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