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