]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiSpellchecker.cpp
#7952 add a check for current buffer to detect buffer switches while in spell checker...
[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                         bv->processUpdateFlags(Update::Force | Update::FitCursor);      
288                 }
289         }
290 }
291
292 void SpellcheckerWidget::Private::setSelection(
293         DocIterator const & from, DocIterator const & to) const
294 {
295         BufferView * bv = gv_->documentBufferView();
296         DocIterator end = to;
297
298         // spell checker corrections may have invalidated the end
299         end.fixIfBroken();
300
301         if (from.pit() != end.pit()) {
302                 // there are multiple paragraphs in selection 
303                 Cursor & bvcur = bv->cursor();
304                 bvcur.setCursor(from);
305                 bvcur.clearSelection();
306                 bvcur.setSelection(true);
307                 bvcur.setCursor(end);
308                 bvcur.setSelection(true);
309         } else {
310                 // FIXME LFUN
311                 // If we used a LFUN, dispatch would do all of this for us
312                 int const size = end.pos() - from.pos();
313                 bv->putSelectionAt(from, size, false);
314         }
315         bv->processUpdateFlags(Update::Force | Update::FitCursor);      
316 }
317
318 void SpellcheckerWidget::Private::forward()
319 {
320         BufferView * bv = gv_->documentBufferView();
321         DocIterator const from = bv->cursor();
322
323         dispatch(FuncRequest(LFUN_ESCAPE));
324         if (!atLastPos(bv->cursor())) {
325                 dispatch(FuncRequest(LFUN_CHAR_FORWARD));
326         }
327         if (atLastPos(bv->cursor())) {
328                 return;
329         }
330         if (from == bv->cursor()) {
331                 //FIXME we must be at the end of a cell
332                 dispatch(FuncRequest(LFUN_CHAR_FORWARD));
333         }
334         if (isWrapAround(bv->cursor())) {
335                 hide();
336         }
337 }
338
339
340 void SpellcheckerWidget::on_languageCO_activated(int index)
341 {
342         string const lang =
343                 fromqstr(d->ui.languageCO->itemData(index).toString());
344         if (!d->word_.lang() || d->word_.lang()->lang() == lang)
345                 // nothing changed
346                 return;
347         dispatch(FuncRequest(LFUN_LANGUAGE, lang));
348         d->check();
349 }
350
351
352 bool SpellcheckerWidget::initialiseParams(std::string const &)
353 {
354         BufferView * bv = d->gv_->documentBufferView();
355         if (bv == 0)
356                 return false;
357         std::set<Language const *> languages = 
358                 bv->buffer().masterBuffer()->getLanguages();
359         if (!languages.empty())
360                 d->setLanguage(*languages.begin());
361         d->start_ = DocIterator();
362         d->wrapAround(false);
363         d->canCheck();
364         return true;
365 }
366
367
368 void SpellcheckerWidget::on_ignoreAllPB_clicked()
369 {
370         /// ignore all occurrences of word
371         if (d->inCheck())
372                 return;
373         LYXERR(Debug::GUI, "Spellchecker: ignore all button");
374         if (d->word_.lang() && !d->word_.word().empty())
375                 theSpellChecker()->accept(d->word_);
376         d->forward();
377         d->check();
378         d->canCheck();
379 }
380
381
382 void SpellcheckerWidget::on_addPB_clicked()
383 {
384         /// insert word in personal dictionary
385         if (d->inCheck())
386                 return;
387         LYXERR(Debug::GUI, "Spellchecker: add word button");
388         theSpellChecker()->insert(d->word_);
389         d->forward();
390         d->check();
391         d->canCheck();
392 }
393
394
395 void SpellcheckerWidget::on_ignorePB_clicked()
396 {
397         /// ignore this occurrence of word
398         if (d->inCheck())
399                 return;
400         LYXERR(Debug::GUI, "Spellchecker: ignore button");
401         d->forward();
402         d->check();
403         d->canCheck();
404 }
405
406
407 void SpellcheckerWidget::on_findNextPB_clicked()
408 {
409         if (d->inCheck())
410                 return;
411         docstring const textfield = qstring_to_ucs4(d->ui.wordED->text());
412         docstring const datastring = find2string(textfield,
413                                 true, true, true);
414         LYXERR(Debug::GUI, "Spellchecker: find next (" << textfield << ")");
415         dispatch(FuncRequest(LFUN_WORD_FIND, datastring));
416         d->canCheck();
417 }
418
419
420 void SpellcheckerWidget::on_replacePB_clicked()
421 {
422         if (d->inCheck())
423                 return;
424         docstring const textfield = qstring_to_ucs4(d->ui.wordED->text());
425         docstring const replacement = qstring_to_ucs4(d->ui.replaceCO->currentText());
426         docstring const datastring = replace2string(replacement, textfield,
427                 true, true, false, false);
428
429         LYXERR(Debug::GUI, "Replace (" << replacement << ")");
430         dispatch(FuncRequest(LFUN_WORD_REPLACE, datastring));
431         d->forward();
432         d->check();
433         d->canCheck();
434 }
435
436
437 void SpellcheckerWidget::on_replaceAllPB_clicked()
438 {
439         if (d->inCheck())
440                 return;
441         docstring const textfield = qstring_to_ucs4(d->ui.wordED->text());
442         docstring const replacement = qstring_to_ucs4(d->ui.replaceCO->currentText());
443         docstring const datastring = replace2string(replacement, textfield,
444                 true, true, true, true);
445
446         LYXERR(Debug::GUI, "Replace all (" << replacement << ")");
447         dispatch(FuncRequest(LFUN_WORD_REPLACE, datastring));
448         d->forward();
449         d->check(); // continue spellchecking
450         d->canCheck();
451 }
452
453
454 void SpellcheckerWidget::Private::updateSuggestions(docstring_list & words)
455 {
456         QString const suggestion = toqstr(word_.word());
457         ui.wordED->setText(suggestion);
458         QListWidget * lw = ui.suggestionsLW;
459         lw->clear();
460
461         if (words.empty()) {
462                 p->on_suggestionsLW_itemClicked(new QListWidgetItem(suggestion));
463                 return;
464         }
465         for (size_t i = 0; i != words.size(); ++i)
466                 lw->addItem(toqstr(words[i]));
467
468         p->on_suggestionsLW_itemClicked(lw->item(0));
469         lw->setCurrentRow(0);
470 }
471
472
473 void SpellcheckerWidget::Private::setLanguage(Language const * lang)
474 {
475         int const pos = ui.languageCO->findData(toqstr(lang->lang()));
476         if (pos != -1)
477                 ui.languageCO->setCurrentIndex(pos);
478 }
479
480
481 void SpellcheckerWidget::Private::check()
482 {
483         BufferView * bv = gv_->documentBufferView();
484         if (!bv || bv->buffer().text().empty())
485                 return;
486
487         DocIterator from = bv->cursor();
488         DocIterator to = isCurrentBuffer(from) ? end_ : doc_iterator_end(&bv->buffer());
489         WordLangTuple word_lang;
490         docstring_list suggestions;
491
492         LYXERR(Debug::GUI, "Spellchecker: start check at " << from);
493         try {
494                 bv->buffer().spellCheck(from, to, word_lang, suggestions);
495         } catch (ExceptionMessage const & message) {
496                 if (message.type_ == WarningException) {
497                         Alert::warning(message.title_, message.details_);
498                         return;
499                 }
500                 throw message;
501         }
502
503         // end of document or selection?
504         if (atLastPos(from)) {
505                 if (isWrapAround()) {
506                         hide();
507                         return;
508                 }
509                 if (continueFromBeginning())
510                         check();
511                 return;
512         }
513
514         if (isWrapAround(from)) {
515                 hide();
516                 return;
517         }
518
519         word_ = word_lang;
520
521         // set suggestions
522         updateSuggestions(suggestions);
523         // set language
524         if (!word_lang.lang())
525                 return;
526         setLanguage(word_lang.lang());
527         // mark misspelled word
528         setSelection(from, to);
529 }
530
531
532 GuiSpellchecker::GuiSpellchecker(GuiView & parent,
533                 Qt::DockWidgetArea area, Qt::WindowFlags flags)
534         : DockView(parent, "spellchecker", qt_("Spellchecker"),
535                    area, flags)
536 {
537         widget_ = new SpellcheckerWidget(&parent, this);
538         setWidget(widget_);
539         setFocusProxy(widget_);
540 }
541
542
543 GuiSpellchecker::~GuiSpellchecker()
544 {
545         setFocusProxy(0);
546         delete widget_;
547 }
548
549
550 void GuiSpellchecker::updateView()
551 {
552         widget_->updateView();
553 }
554
555
556 Dialog * createGuiSpellchecker(GuiView & lv) 
557
558         GuiSpellchecker * gui = new GuiSpellchecker(lv, Qt::RightDockWidgetArea);
559 #ifdef Q_WS_MACX
560         gui->setFloating(true);
561 #endif
562         return gui;
563 }
564
565
566 } // namespace frontend
567 } // namespace lyx
568
569 #include "moc_GuiSpellchecker.cpp"