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