1 /**
2 * highlighter.cpp
3 *
4 * Copyright (C) 2004 Zack Rusin <zack@kde.org>
5 * Copyright (C) 2006 Laurent Montel <montel@kde.org>
6 * Copyright (C) 2013 Martin Sandsmark <martin.sandsmark@org>
7 *
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Lesser General Public
10 * License as published by the Free Software Foundation; either
11 * version 2.1 of the License, or (at your option) any later version.
12 *
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Lesser General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public
19 * License along with this library; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 * 02110-1301 USA
22 */
23
24 #include "highlighter.h"
25
26 #include "../core/speller.h"
27 #include "../core/loader_p.h"
28 #include "../core/tokenizer_p.h"
29 #include "../core/settings_p.h"
30
31 #include <QTextEdit>
32 #include <QTextCharFormat>
33 #include <QTimer>
34 #include <QColor>
35 #include <QHash>
36 #include <QTextCursor>
37 #include <QEvent>
38 #include <QKeyEvent>
39 #include <QApplication>
40 #include <QMetaMethod>
41 #include <QPlainTextEdit>
42 #include <QDebug>
43
44 namespace Sonnet
45 {
46
47 class LanguageCache : public QTextBlockUserData {
48 public:
49 QMap<QPair<int,int>, QString> languages;
invalidate(int pos)50 void invalidate(int pos) {
51 QMutableMapIterator<QPair<int,int>, QString> it(languages);
52 it.toBack();
53 while (it.hasPrevious()) {
54 it.previous();
55 if (it.key().first+it.key().second >=pos) it.remove();
56 else break;
57 }
58 }
59 };
60
61
62 class HighlighterPrivate
63 {
64 public:
HighlighterPrivate(Highlighter * qq,const QColor & col)65 HighlighterPrivate(Highlighter *qq, const QColor &col)
66 : textEdit(nullptr),
67 plainTextEdit(nullptr),
68 spellColor(col),
69 q(qq)
70 {
71 tokenizer = new WordTokenizer();
72 active = true;
73 automatic = false;
74 connected = false;
75 wordCount = 0;
76 errorCount = 0;
77 intraWordEditing = false;
78 completeRehighlightRequired = false;
79 spellColor = spellColor.isValid() ? spellColor : Qt::red;
80
81 loader = Loader::openLoader();
82 loader->settings()->restore();
83
84 spellchecker = new Sonnet::Speller();
85 spellCheckerFound = spellchecker->isValid();
86 rehighlightRequest = new QTimer(q);
87 q->connect(rehighlightRequest, SIGNAL(timeout()),
88 q, SLOT(slotRehighlight()));
89
90 if (!spellCheckerFound) {
91 return;
92 }
93
94 disablePercentage = loader->settings()->disablePercentageWordError();
95 disableWordCount = loader->settings()->disableWordErrorCount();
96
97 completeRehighlightRequired = true;
98 rehighlightRequest->setInterval(0);
99 rehighlightRequest->setSingleShot(true);
100 rehighlightRequest->start();
101 }
102
103 ~HighlighterPrivate();
104 WordTokenizer *tokenizer;
105 Loader *loader;
106 Speller *spellchecker;
107 QTextEdit *textEdit;
108 QPlainTextEdit *plainTextEdit;
109 bool active;
110 bool automatic;
111 bool completeRehighlightRequired;
112 bool intraWordEditing;
113 bool spellCheckerFound; //cached d->dict->isValid() value
114 bool connected;
115 int disablePercentage;
116 int disableWordCount;
117 int wordCount, errorCount;
118 QTimer *rehighlightRequest;
119 QColor spellColor;
120 Highlighter *q;
121 };
122
~HighlighterPrivate()123 HighlighterPrivate::~HighlighterPrivate()
124 {
125 delete spellchecker;
126 delete tokenizer;
127 }
128
Highlighter(QTextEdit * edit,const QColor & _col)129 Highlighter::Highlighter(QTextEdit *edit,
130 const QColor &_col)
131 : QSyntaxHighlighter(edit),
132 d(new HighlighterPrivate(this, _col))
133 {
134 d->textEdit = edit;
135 d->textEdit->installEventFilter(this);
136 d->textEdit->viewport()->installEventFilter(this);
137 }
138
Highlighter(QPlainTextEdit * edit,const QColor & col)139 Highlighter::Highlighter(QPlainTextEdit *edit, const QColor &col)
140 : QSyntaxHighlighter(edit),
141 d(new HighlighterPrivate(this, col))
142 {
143 d->plainTextEdit = edit;
144 setDocument(d->plainTextEdit->document());
145 d->plainTextEdit->installEventFilter(this);
146 d->plainTextEdit->viewport()->installEventFilter(this);
147 }
148
~Highlighter()149 Highlighter::~Highlighter()
150 {
151 delete d;
152 }
153
spellCheckerFound() const154 bool Highlighter::spellCheckerFound() const
155 {
156 return d->spellCheckerFound;
157 }
158
slotRehighlight()159 void Highlighter::slotRehighlight()
160 {
161 if (d->completeRehighlightRequired) {
162 d->wordCount = 0;
163 d->errorCount = 0;
164 rehighlight();
165
166 } else {
167 //rehighlight the current para only (undo/redo safe)
168 QTextCursor cursor;
169 if (d->textEdit)
170 cursor = d->textEdit->textCursor();
171 else
172 cursor = d->plainTextEdit->textCursor();
173 cursor.insertText(QString());
174 }
175 //if (d->checksDone == d->checksRequested)
176 //d->completeRehighlightRequired = false;
177 QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
178 }
179
automatic() const180 bool Highlighter::automatic() const
181 {
182 return d->automatic;
183 }
184
intraWordEditing() const185 bool Highlighter::intraWordEditing() const
186 {
187 return d->intraWordEditing;
188 }
189
setIntraWordEditing(bool editing)190 void Highlighter::setIntraWordEditing(bool editing)
191 {
192 d->intraWordEditing = editing;
193 }
194
setAutomatic(bool automatic)195 void Highlighter::setAutomatic(bool automatic)
196 {
197 if (automatic == d->automatic) {
198 return;
199 }
200
201 d->automatic = automatic;
202 if (d->automatic) {
203 slotAutoDetection();
204 }
205 }
206
slotAutoDetection()207 void Highlighter::slotAutoDetection()
208 {
209 bool savedActive = d->active;
210
211 //don't disable just because 1 of 4 is misspelled.
212 if (d->automatic && d->wordCount >= 10) {
213 // tme = Too many errors
214 bool tme = (d->errorCount >= d->disableWordCount) && (
215 d->errorCount * 100 >= d->disablePercentage * d->wordCount);
216 if (d->active && tme) {
217 d->active = false;
218 } else if (!d->active && !tme) {
219 d->active = true;
220 }
221 }
222
223 if (d->active != savedActive) {
224 if (d->active) {
225 emit activeChanged(tr("As-you-type spell checking enabled."));
226 } else {
227 qDebug() << "Sonnet: Disabling spell checking, too many errors";
228 emit activeChanged(tr("Too many misspelled words. "
229 "As-you-type spell checking disabled."));
230 }
231
232 d->completeRehighlightRequired = true;
233 d->rehighlightRequest->setInterval(100);
234 d->rehighlightRequest->setSingleShot(true);
235 }
236 }
237
setActive(bool active)238 void Highlighter::setActive(bool active)
239 {
240 if (active == d->active) {
241 return;
242 }
243 d->active = active;
244 rehighlight();
245
246 if (d->active) {
247 emit activeChanged(tr("As-you-type spell checking enabled."));
248 } else {
249 emit activeChanged(tr("As-you-type spell checking disabled."));
250 }
251 }
252
isActive() const253 bool Highlighter::isActive() const
254 {
255 return d->active;
256 }
257
contentsChange(int pos,int add,int rem)258 void Highlighter::contentsChange(int pos, int add, int rem)
259 {
260 // Invalidate the cache where the text has changed
261 const QTextBlock &lastBlock = document()->findBlock(pos + add - rem);
262 QTextBlock block = document()->findBlock(pos);
263 do {
264 LanguageCache* cache=dynamic_cast<LanguageCache*>(block.userData());
265 if (cache) cache->invalidate(pos-block.position());
266 block = block.next();
267 } while (block.isValid() && block < lastBlock);
268 }
269
highlightBlock(const QString & text)270 void Highlighter::highlightBlock(const QString &text)
271 {
272 if (text.isEmpty() || !d->active || !d->spellCheckerFound) {
273 return;
274 }
275
276 if (!d->connected) {
277 connect(document(), SIGNAL(contentsChange(int,int,int)),
278 SLOT(contentsChange(int,int,int)));
279 d->connected = true;
280 }
281 QTextCursor cursor;
282 if (d->textEdit) {
283 cursor = d->textEdit->textCursor();
284 } else {
285 cursor = d->plainTextEdit->textCursor();
286 }
287 int index = cursor.position();
288
289 const int lengthPosition = text.length() - 1;
290
291 if ( index != lengthPosition ||
292 ( lengthPosition > 0 && !text[lengthPosition-1].isLetter() ) ) {
293 LanguageCache* cache=dynamic_cast<LanguageCache*>(currentBlockUserData());
294 if (!cache) {
295 cache = new LanguageCache;
296 setCurrentBlockUserData(cache);
297 }
298
299 QStringRef sentence=&text;
300
301 d->tokenizer->setBuffer(sentence.toString());
302 int offset=sentence.position();
303 while (d->tokenizer->hasNext()) {
304 QStringRef word=d->tokenizer->next();
305 if (!d->tokenizer->isSpellcheckable()) continue;
306 ++d->wordCount;
307 if (d->spellchecker->isMisspelled(word.toString())) {
308 ++d->errorCount;
309 setMisspelled(word.position()+offset, word.length());
310 } else {
311 unsetMisspelled(word.position()+offset, word.length());
312 }
313 }
314 }
315 //QTimer::singleShot( 0, this, SLOT(checkWords()) );
316 setCurrentBlockState(0);
317 }
318
currentLanguage() const319 QString Highlighter::currentLanguage() const
320 {
321 return d->spellchecker->language();
322 }
323
setCurrentLanguage(const QString & lang)324 void Highlighter::setCurrentLanguage(const QString &lang)
325 {
326 QString prevLang=d->spellchecker->language();
327 d->spellchecker->setLanguage(lang);
328 d->spellCheckerFound = d->spellchecker->isValid();
329 if (!d->spellCheckerFound) {
330 qDebug() << "No dictionary for \""
331 << lang
332 << "\" staying with the current language.";
333 d->spellchecker->setLanguage(prevLang);
334 return;
335 }
336 d->wordCount = 0;
337 d->errorCount = 0;
338 if (d->automatic) {
339 d->rehighlightRequest->start(0);
340 }
341 }
342
setMisspelled(int start,int count)343 void Highlighter::setMisspelled(int start, int count)
344 {
345 QTextCharFormat format;
346 format.setFontUnderline(true);
347 format.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
348 format.setUnderlineColor(d->spellColor);
349 setFormat(start, count, format);
350 }
351
unsetMisspelled(int start,int count)352 void Highlighter::unsetMisspelled(int start, int count)
353 {
354 setFormat(start, count, QTextCharFormat());
355 }
356
eventFilter(QObject * o,QEvent * e)357 bool Highlighter::eventFilter(QObject *o, QEvent *e)
358 {
359 if (!d->spellCheckerFound) {
360 return false;
361 }
362 if ((o == d->textEdit || o == d->plainTextEdit) && (e->type() == QEvent::KeyPress)) {
363 QKeyEvent *k = static_cast<QKeyEvent *>(e);
364 //d->autoReady = true;
365 if (d->rehighlightRequest->isActive()) { // try to stay out of the users way
366 d->rehighlightRequest->start(500);
367 }
368 if (k->key() == Qt::Key_Enter ||
369 k->key() == Qt::Key_Return ||
370 k->key() == Qt::Key_Up ||
371 k->key() == Qt::Key_Down ||
372 k->key() == Qt::Key_Left ||
373 k->key() == Qt::Key_Right ||
374 k->key() == Qt::Key_PageUp ||
375 k->key() == Qt::Key_PageDown ||
376 k->key() == Qt::Key_Home ||
377 k->key() == Qt::Key_End ||
378 ((k->modifiers() == Qt::ControlModifier) &&
379 ((k->key() == Qt::Key_A) ||
380 (k->key() == Qt::Key_B) ||
381 (k->key() == Qt::Key_E) ||
382 (k->key() == Qt::Key_N) ||
383 (k->key() == Qt::Key_P)))) {
384 if (intraWordEditing()) {
385 setIntraWordEditing(false);
386 d->completeRehighlightRequired = true;
387 d->rehighlightRequest->setInterval(500);
388 d->rehighlightRequest->setSingleShot(true);
389 d->rehighlightRequest->start();
390 }
391 } else {
392 setIntraWordEditing(true);
393 }
394 if (k->key() == Qt::Key_Space ||
395 k->key() == Qt::Key_Enter ||
396 k->key() == Qt::Key_Return) {
397 QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
398 }
399 }
400
401 else if ((( d->textEdit && ( o == d->textEdit->viewport())) || (d->plainTextEdit && (o == d->plainTextEdit->viewport()))) &&
402 (e->type() == QEvent::MouseButtonPress)) {
403 //d->autoReady = true;
404 if (intraWordEditing()) {
405 setIntraWordEditing(false);
406 d->completeRehighlightRequired = true;
407 d->rehighlightRequest->setInterval(0);
408 d->rehighlightRequest->setSingleShot(true);
409 d->rehighlightRequest->start();
410 }
411 }
412 return false;
413 }
414
addWordToDictionary(const QString & word)415 void Highlighter::addWordToDictionary(const QString &word)
416 {
417 d->spellchecker->addToPersonal(word);
418 }
419
ignoreWord(const QString & word)420 void Highlighter::ignoreWord(const QString &word)
421 {
422 d->spellchecker->addToSession(word);
423 }
424
suggestionsForWord(const QString & word,int max)425 QStringList Highlighter::suggestionsForWord(const QString &word, int max)
426 {
427 QStringList suggestions = d->spellchecker->suggest(word);
428 if (max != -1) {
429 while (suggestions.count() > max) {
430 suggestions.removeLast();
431 }
432 }
433 return suggestions;
434 }
435
isWordMisspelled(const QString & word)436 bool Highlighter::isWordMisspelled(const QString &word)
437 {
438 return d->spellchecker->isMisspelled(word);
439 }
440
setMisspelledColor(const QColor & color)441 void Highlighter::setMisspelledColor(const QColor &color)
442 {
443 d->spellColor = color;
444 }
445
checkerEnabledByDefault() const446 bool Highlighter::checkerEnabledByDefault() const
447 {
448 return d->loader->settings()->checkerEnabledByDefault();
449 }
450
setDocument(QTextDocument * document)451 void Highlighter::setDocument(QTextDocument* document)
452 {
453 d->connected = false;
454 QSyntaxHighlighter::setDocument(document);
455 }
456
457 }
458