Sonnet

highlighter.cpp
1/*
2 * highlighter.cpp
3 *
4 * SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
5 * SPDX-FileCopyrightText: 2006 Laurent Montel <montel@kde.org>
6 * SPDX-FileCopyrightText: 2013 Martin Sandsmark <martin.sandsmark@org>
7 *
8 * SPDX-License-Identifier: LGPL-2.1-or-later
9 */
10
11#include "highlighter.h"
12
13#include "languagefilter_p.h"
14#include "loader_p.h"
15#include "settingsimpl_p.h"
16#include "speller.h"
17#include "tokenizer_p.h"
18
19#include "ui_debug.h"
20
21#include <QColor>
22#include <QEvent>
23#include <QHash>
24#include <QKeyEvent>
25#include <QMetaMethod>
26#include <QPlainTextEdit>
27#include <QTextCharFormat>
28#include <QTextCursor>
29#include <QTextEdit>
30#include <QTimer>
31
32namespace Sonnet
33{
34// Cache of previously-determined languages (when using AutoDetectLanguage)
35// There is one such cache per block (paragraph)
36class LanguageCache : public QTextBlockUserData
37{
38public:
39 // Key: QPair<start, length>
40 // Value: language name
41 QMap<QPair<int, int>, QString> languages;
42
43 // Remove all cached language information after @p pos
44 void invalidate(int pos)
45 {
47 it.toBack();
48 while (it.hasPrevious()) {
49 it.previous();
50 if (it.key().first + it.key().second >= pos) {
51 it.remove();
52 } else {
53 break;
54 }
55 }
56 }
57
58 QString languageAtPos(int pos) const
59 {
60 // The data structure isn't really great for such lookups...
62 while (it.hasNext()) {
63 it.next();
64 if (it.key().first <= pos && it.key().first + it.key().second >= pos) {
65 return it.value();
66 }
67 }
68 return QString();
69 }
70};
71
72class HighlighterPrivate
73{
74public:
75 HighlighterPrivate(Highlighter *qq, const QColor &col)
76 : textEdit(nullptr)
77 , plainTextEdit(nullptr)
78 , spellColor(col)
79 , q(qq)
80 {
81 tokenizer = new WordTokenizer();
82 active = true;
83 automatic = false;
84 autoDetectLanguageDisabled = false;
85 wordCount = 0;
86 errorCount = 0;
87 intraWordEditing = false;
88 completeRehighlightRequired = false;
89 spellColor = spellColor.isValid() ? spellColor : Qt::red;
90 languageFilter = new LanguageFilter(new SentenceTokenizer());
91
92 loader = Loader::openLoader();
93 loader->settings()->restore();
94
95 spellchecker = new Sonnet::Speller();
96 spellCheckerFound = spellchecker->isValid();
97 rehighlightRequest = new QTimer(q);
98 q->connect(rehighlightRequest, &QTimer::timeout, q, &Highlighter::slotRehighlight);
99
100 if (!spellCheckerFound) {
101 return;
102 }
103
104 disablePercentage = loader->settings()->disablePercentageWordError();
105 disableWordCount = loader->settings()->disableWordErrorCount();
106
107 completeRehighlightRequired = true;
108 rehighlightRequest->setInterval(0);
109 rehighlightRequest->setSingleShot(true);
110 rehighlightRequest->start();
111 }
112
113 ~HighlighterPrivate();
114 WordTokenizer *tokenizer = nullptr;
115 LanguageFilter *languageFilter = nullptr;
116 Loader *loader = nullptr;
117 Speller *spellchecker = nullptr;
118 QTextEdit *textEdit = nullptr;
119 QPlainTextEdit *plainTextEdit = nullptr;
120 bool active;
121 bool automatic;
122 bool autoDetectLanguageDisabled;
123 bool completeRehighlightRequired;
124 bool intraWordEditing;
125 bool spellCheckerFound; // cached d->dict->isValid() value
126 QMetaObject::Connection contentsChangeConnection;
127 int disablePercentage = 0;
128 int disableWordCount = 0;
129 int wordCount, errorCount;
130 QTimer *rehighlightRequest = nullptr;
131 QColor spellColor;
132 Highlighter *const q;
133};
134
135HighlighterPrivate::~HighlighterPrivate()
136{
137 delete spellchecker;
138 delete languageFilter;
139 delete tokenizer;
140}
141
142Highlighter::Highlighter(QTextEdit *edit, const QColor &_col)
143 : QSyntaxHighlighter(edit)
144 , d(new HighlighterPrivate(this, _col))
145{
146 d->textEdit = edit;
147 d->textEdit->installEventFilter(this);
148 d->textEdit->viewport()->installEventFilter(this);
149}
150
151Highlighter::Highlighter(QPlainTextEdit *edit, const QColor &col)
152 : QSyntaxHighlighter(edit)
153 , d(new HighlighterPrivate(this, col))
154{
155 d->plainTextEdit = edit;
156 setDocument(d->plainTextEdit->document());
157 d->plainTextEdit->installEventFilter(this);
158 d->plainTextEdit->viewport()->installEventFilter(this);
159}
160
161Highlighter::~Highlighter()
162{
163 if (d->contentsChangeConnection) {
164 // prevent crash from QSyntaxHighlighter::~QSyntaxHighlighter -> (...) -> QTextDocument::contentsChange() signal emission:
165 // ASSERT failure in Sonnet::Highlighter: "Called object is not of the correct type (class destructor may have already run)"
166 QObject::disconnect(d->contentsChangeConnection);
167 }
168}
169
171{
172 return d->spellCheckerFound;
173}
174
176{
177 if (d->completeRehighlightRequired) {
178 d->wordCount = 0;
179 d->errorCount = 0;
180 rehighlight();
181 } else {
182 // rehighlight the current para only (undo/redo safe)
183 QTextCursor cursor;
184 if (d->textEdit) {
185 cursor = d->textEdit->textCursor();
186 } else {
187 cursor = d->plainTextEdit->textCursor();
188 }
189 if (cursor.hasSelection()) {
190 cursor.clearSelection();
191 }
192 cursor.insertText(QString());
193 }
194 // if (d->checksDone == d->checksRequested)
195 // d->completeRehighlightRequired = false;
196 QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
197}
198
200{
201 return d->automatic;
202}
203
205{
206 return d->autoDetectLanguageDisabled;
207}
208
209bool Highlighter::intraWordEditing() const
210{
211 return d->intraWordEditing;
212}
213
214void Highlighter::setIntraWordEditing(bool editing)
215{
216 d->intraWordEditing = editing;
217}
218
219void Highlighter::setAutomatic(bool automatic)
220{
221 if (automatic == d->automatic) {
222 return;
223 }
224
225 d->automatic = automatic;
226 if (d->automatic) {
228 }
229}
230
232{
233 d->autoDetectLanguageDisabled = autoDetectDisabled;
234}
235
237{
238 bool savedActive = d->active;
239
240 // don't disable just because 1 of 4 is misspelled.
241 if (d->automatic && d->wordCount >= 10) {
242 // tme = Too many errors
243 /* clang-format off */
244 bool tme = (d->errorCount >= d->disableWordCount)
245 && (d->errorCount * 100 >= d->disablePercentage * d->wordCount);
246 /* clang-format on */
247
248 if (d->active && tme) {
249 d->active = false;
250 } else if (!d->active && !tme) {
251 d->active = true;
252 }
253 }
254
255 if (d->active != savedActive) {
256 if (d->active) {
257 Q_EMIT activeChanged(tr("As-you-type spell checking enabled."));
258 } else {
259 qCDebug(SONNET_LOG_UI) << "Sonnet: Disabling spell checking, too many errors";
261 tr("Too many misspelled words. "
262 "As-you-type spell checking disabled."));
263 }
264
265 d->completeRehighlightRequired = true;
266 d->rehighlightRequest->setInterval(100);
267 d->rehighlightRequest->setSingleShot(true);
268 }
269}
270
271void Highlighter::setActive(bool active)
272{
273 if (active == d->active) {
274 return;
275 }
276 d->active = active;
277 rehighlight();
278
279 if (d->active) {
280 Q_EMIT activeChanged(tr("As-you-type spell checking enabled."));
281 } else {
282 Q_EMIT activeChanged(tr("As-you-type spell checking disabled."));
283 }
284}
285
287{
288 return d->active;
289}
290
291void Highlighter::contentsChange(int pos, int add, int rem)
292{
293 // Invalidate the cache where the text has changed
294 const QTextBlock &lastBlock = document()->findBlock(pos + add - rem);
295 QTextBlock block = document()->findBlock(pos);
296 do {
297 LanguageCache *cache = dynamic_cast<LanguageCache *>(block.userData());
298 if (cache) {
299 cache->invalidate(pos - block.position());
300 }
301 block = block.next();
302 } while (block.isValid() && block < lastBlock);
303}
304
305static bool hasNotEmptyText(const QString &text)
306{
307 for (int i = 0; i < text.length(); ++i) {
308 if (!text.at(i).isSpace()) {
309 return true;
310 }
311 }
312 return false;
313}
314
315void Highlighter::highlightBlock(const QString &text)
316{
317 if (!hasNotEmptyText(text) || !d->active || !d->spellCheckerFound) {
318 return;
319 }
320
321 if (!d->contentsChangeConnection) {
322 d->contentsChangeConnection = connect(document(), &QTextDocument::contentsChange, this, &Highlighter::contentsChange);
323 }
324
325 d->languageFilter->setBuffer(text);
326
327 LanguageCache *cache = dynamic_cast<LanguageCache *>(currentBlockUserData());
328 if (!cache) {
329 cache = new LanguageCache;
331 }
332
333 const bool autodetectLanguage = d->spellchecker->testAttribute(Speller::AutoDetectLanguage);
334 while (d->languageFilter->hasNext()) {
335 Token sentence = d->languageFilter->next();
336 if (autodetectLanguage && !d->autoDetectLanguageDisabled) {
337 QString lang;
338 QPair<int, int> spos = QPair<int, int>(sentence.position(), sentence.length());
339 // try cache first
340 if (cache->languages.contains(spos)) {
341 lang = cache->languages.value(spos);
342 } else {
343 lang = d->languageFilter->language();
344 if (!d->languageFilter->isSpellcheckable()) {
345 lang.clear();
346 }
347 cache->languages[spos] = lang;
348 }
349 if (lang.isEmpty()) {
350 continue;
351 }
352 d->spellchecker->setLanguage(lang);
353 }
354
355 d->tokenizer->setBuffer(sentence.toString());
356 int offset = sentence.position();
357 while (d->tokenizer->hasNext()) {
358 Token word = d->tokenizer->next();
359 if (!d->tokenizer->isSpellcheckable()) {
360 continue;
361 }
362 ++d->wordCount;
363 if (d->spellchecker->isMisspelled(word.toString())) {
364 ++d->errorCount;
365 setMisspelled(word.position() + offset, word.length());
366 } else {
367 unsetMisspelled(word.position() + offset, word.length());
368 }
369 }
370 }
371 // QTimer::singleShot( 0, this, SLOT(checkWords()) );
373}
374
376{
377 return d->spellchecker->language();
378}
379
381{
382 QString prevLang = d->spellchecker->language();
383 d->spellchecker->setLanguage(lang);
384 d->spellCheckerFound = d->spellchecker->isValid();
385 if (!d->spellCheckerFound) {
386 qCDebug(SONNET_LOG_UI) << "No dictionary for \"" << lang << "\" staying with the current language.";
387 d->spellchecker->setLanguage(prevLang);
388 return;
389 }
390 d->wordCount = 0;
391 d->errorCount = 0;
392 if (d->automatic || d->active) {
393 d->rehighlightRequest->start(0);
394 }
395}
396
397void Highlighter::setMisspelled(int start, int count)
398{
402 format.setUnderlineColor(d->spellColor);
403 setFormat(start, count, format);
404}
405
406void Highlighter::unsetMisspelled(int start, int count)
407{
409}
410
411bool Highlighter::eventFilter(QObject *o, QEvent *e)
412{
413 if (!d->spellCheckerFound) {
414 return false;
415 }
416 if ((o == d->textEdit || o == d->plainTextEdit) && (e->type() == QEvent::KeyPress)) {
417 QKeyEvent *k = static_cast<QKeyEvent *>(e);
418 // d->autoReady = true;
419 if (d->rehighlightRequest->isActive()) { // try to stay out of the users way
420 d->rehighlightRequest->start(500);
421 }
422 /* clang-format off */
423 if (k->key() == Qt::Key_Enter
424 || k->key() == Qt::Key_Return
425 || k->key() == Qt::Key_Up
426 || k->key() == Qt::Key_Down
427 || k->key() == Qt::Key_Left
428 || k->key() == Qt::Key_Right
429 || k->key() == Qt::Key_PageUp
430 || k->key() == Qt::Key_PageDown
431 || k->key() == Qt::Key_Home
432 || k->key() == Qt::Key_End
434 && (k->key() == Qt::Key_A
435 || k->key() == Qt::Key_B
436 || k->key() == Qt::Key_E
437 || k->key() == Qt::Key_N
438 || k->key() == Qt::Key_P))) { /* clang-format on */
439 if (intraWordEditing()) {
440 setIntraWordEditing(false);
441 d->completeRehighlightRequired = true;
442 d->rehighlightRequest->setInterval(500);
443 d->rehighlightRequest->setSingleShot(true);
444 d->rehighlightRequest->start();
445 }
446 } else {
447 setIntraWordEditing(true);
448 }
449 if (k->key() == Qt::Key_Space //
450 || k->key() == Qt::Key_Enter //
451 || k->key() == Qt::Key_Return) {
452 QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
453 }
454 } else if (((d->textEdit && (o == d->textEdit->viewport())) //
455 || (d->plainTextEdit && (o == d->plainTextEdit->viewport()))) //
456 && (e->type() == QEvent::MouseButtonPress)) {
457 // d->autoReady = true;
458 if (intraWordEditing()) {
459 setIntraWordEditing(false);
460 d->completeRehighlightRequired = true;
461 d->rehighlightRequest->setInterval(0);
462 d->rehighlightRequest->setSingleShot(true);
463 d->rehighlightRequest->start();
464 }
465 }
466 return false;
467}
468
470{
471 d->spellchecker->addToPersonal(word);
472}
473
475{
476 d->spellchecker->addToSession(word);
477}
478
480{
481 QStringList suggestions = d->spellchecker->suggest(word);
482 if (max >= 0 && suggestions.count() > max) {
483 suggestions = suggestions.mid(0, max);
484 }
485 return suggestions;
486}
487
489{
490 LanguageCache *cache = dynamic_cast<LanguageCache *>(cursor.block().userData());
491 if (cache) {
492 const QString cachedLanguage = cache->languageAtPos(cursor.positionInBlock());
493 if (!cachedLanguage.isEmpty()) {
494 d->spellchecker->setLanguage(cachedLanguage);
495 }
496 }
497 QStringList suggestions = d->spellchecker->suggest(word);
498 if (max >= 0 && suggestions.count() > max) {
499 suggestions = suggestions.mid(0, max);
500 }
501 return suggestions;
502}
503
505{
506 return d->spellchecker->isMisspelled(word);
507}
508
510{
511 d->spellColor = color;
512}
513
515{
516 return d->loader->settings()->checkerEnabledByDefault();
517}
518
520{
521 d->contentsChangeConnection = {};
523}
524}
525
526#include "moc_highlighter.cpp"
bool autoDetectLanguageDisabled() const
Returns whether the automatic language detection is disabled, overriding the Sonnet settings.
void slotAutoDetection()
Run auto detection, disabling spell checking if too many errors are found.
void addWordToDictionary(const QString &word)
Adds the given word permanently to the dictionary.
void slotRehighlight()
Force a new highlighting.
void activeChanged(const QString &description)
Emitted when as-you-type spell checking is enabled or disabled.
QString currentLanguage() const
Returns the current language used for spell checking.
void ignoreWord(const QString &word)
Ignores the given word.
bool isActive() const
Returns the state of spell checking.
bool isWordMisspelled(const QString &word)
Checks if a given word is marked as misspelled by the highlighter.
QStringList suggestionsForWord(const QString &word, int max=10)
Returns a list of suggested replacements for the given misspelled word.
void setActive(bool active)
Enable/Disable spell checking.
bool automatic() const
Returns the state of the automatic disabling of spell checking.
bool spellCheckerFound() const
Returns whether a spell checking backend with support for the currentLanguage was found.
void setMisspelledColor(const QColor &color)
Sets the color in which the highlighter underlines misspelled words.
void setAutomatic(bool automatic)
Sets whether to automatically disable spell checking if there's too many errors.
void setAutoDetectLanguageDisabled(bool autoDetectDisabled)
Sets whether to disable the automatic language detection.
bool checkerEnabledByDefault() const
Return true if checker is enabled by default.
void setDocument(QTextDocument *document)
Set a new QTextDocument for this highlighter to operate on.
void setCurrentLanguage(const QString &language)
Set language to use for spell checking.
Spell checker object.
bool isValid() const
Definition speller.cpp:238
Q_SCRIPTABLE Q_NOREPLY void start()
The sonnet namespace.
bool isSpace(char32_t ucs4)
bool isValid() const const
Type type() const const
int key() const const
Qt::KeyboardModifiers modifiers() const const
qsizetype count() const const
QList< T > mid(qsizetype pos, qsizetype length) const const
bool contains(const Key &key) const const
T value(const Key &key, const T &defaultValue) const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
void installEventFilter(QObject *filterObj)
QString tr(const char *sourceText, const char *disambiguation, int n)
const QChar at(qsizetype position) const const
void clear()
bool isEmpty() const const
qsizetype length() const const
QTextBlockUserData * currentBlockUserData() const const
QTextDocument * document() const const
QTextCharFormat format(int position) const const
void setCurrentBlockState(int newState)
void setCurrentBlockUserData(QTextBlockUserData *data)
void setDocument(QTextDocument *doc)
void setFormat(int start, int count, const QColor &color)
Key_Enter
ControlModifier
bool isValid() const const
QTextBlock next() const const
int position() const const
QTextBlockUserData * userData() const const
void setFontUnderline(bool underline)
void setUnderlineColor(const QColor &color)
void setUnderlineStyle(UnderlineStyle style)
QTextBlock block() const const
void clearSelection()
bool hasSelection() const const
void insertText(const QString &text)
int positionInBlock() const const
void contentsChange(int position, int charsRemoved, int charsAdded)
QTextBlock findBlock(int pos) const const
void setInterval(int msec)
void setSingleShot(bool singleShot)
void start()
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:29:33 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.