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