File indexing completed on 2024-05-05 16:22:13
0001 /* 0002 * highlighter.cpp 0003 * 0004 * SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org> 0005 * SPDX-FileCopyrightText: 2006 Laurent Montel <montel@kde.org> 0006 * SPDX-FileCopyrightText: 2013 Martin Sandsmark <martin.sandsmark@org> 0007 * 0008 * SPDX-License-Identifier: LGPL-2.1-or-later 0009 */ 0010 0011 #include "highlighter.h" 0012 0013 #include "languagefilter_p.h" 0014 #include "loader_p.h" 0015 #include "settingsimpl_p.h" 0016 #include "speller.h" 0017 #include "tokenizer_p.h" 0018 0019 #include "ui_debug.h" 0020 0021 #include <QColor> 0022 #include <QEvent> 0023 #include <QHash> 0024 #include <QKeyEvent> 0025 #include <QMetaMethod> 0026 #include <QPlainTextEdit> 0027 #include <QTextCharFormat> 0028 #include <QTextCursor> 0029 #include <QTextEdit> 0030 #include <QTimer> 0031 0032 namespace Sonnet 0033 { 0034 // Cache of previously-determined languages (when using AutoDetectLanguage) 0035 // There is one such cache per block (paragraph) 0036 class LanguageCache : public QTextBlockUserData 0037 { 0038 public: 0039 // Key: QPair<start, length> 0040 // Value: language name 0041 QMap<QPair<int, int>, QString> languages; 0042 0043 // Remove all cached language information after @p pos 0044 void invalidate(int pos) 0045 { 0046 QMutableMapIterator<QPair<int, int>, QString> it(languages); 0047 it.toBack(); 0048 while (it.hasPrevious()) { 0049 it.previous(); 0050 if (it.key().first + it.key().second >= pos) { 0051 it.remove(); 0052 } else { 0053 break; 0054 } 0055 } 0056 } 0057 0058 QString languageAtPos(int pos) const 0059 { 0060 // The data structure isn't really great for such lookups... 0061 QMapIterator<QPair<int, int>, QString> it(languages); 0062 while (it.hasNext()) { 0063 it.next(); 0064 if (it.key().first <= pos && it.key().first + it.key().second >= pos) { 0065 return it.value(); 0066 } 0067 } 0068 return QString(); 0069 } 0070 }; 0071 0072 class HighlighterPrivate 0073 { 0074 public: 0075 HighlighterPrivate(Highlighter *qq, const QColor &col) 0076 : textEdit(nullptr) 0077 , plainTextEdit(nullptr) 0078 , spellColor(col) 0079 , q(qq) 0080 { 0081 tokenizer = new WordTokenizer(); 0082 active = true; 0083 automatic = false; 0084 autoDetectLanguageDisabled = false; 0085 connected = false; 0086 wordCount = 0; 0087 errorCount = 0; 0088 intraWordEditing = false; 0089 completeRehighlightRequired = false; 0090 spellColor = spellColor.isValid() ? spellColor : Qt::red; 0091 languageFilter = new LanguageFilter(new SentenceTokenizer()); 0092 0093 loader = Loader::openLoader(); 0094 loader->settings()->restore(); 0095 0096 spellchecker = new Sonnet::Speller(); 0097 spellCheckerFound = spellchecker->isValid(); 0098 rehighlightRequest = new QTimer(q); 0099 q->connect(rehighlightRequest, &QTimer::timeout, q, &Highlighter::slotRehighlight); 0100 0101 if (!spellCheckerFound) { 0102 return; 0103 } 0104 0105 disablePercentage = loader->settings()->disablePercentageWordError(); 0106 disableWordCount = loader->settings()->disableWordErrorCount(); 0107 0108 completeRehighlightRequired = true; 0109 rehighlightRequest->setInterval(0); 0110 rehighlightRequest->setSingleShot(true); 0111 rehighlightRequest->start(); 0112 } 0113 0114 ~HighlighterPrivate(); 0115 WordTokenizer *tokenizer = nullptr; 0116 LanguageFilter *languageFilter = nullptr; 0117 Loader *loader = nullptr; 0118 Speller *spellchecker = nullptr; 0119 QTextEdit *textEdit = nullptr; 0120 QPlainTextEdit *plainTextEdit = nullptr; 0121 bool active; 0122 bool automatic; 0123 bool autoDetectLanguageDisabled; 0124 bool completeRehighlightRequired; 0125 bool intraWordEditing; 0126 bool spellCheckerFound; // cached d->dict->isValid() value 0127 bool connected; 0128 int disablePercentage = 0; 0129 int disableWordCount = 0; 0130 int wordCount, errorCount; 0131 QTimer *rehighlightRequest = nullptr; 0132 QColor spellColor; 0133 Highlighter *const q; 0134 }; 0135 0136 HighlighterPrivate::~HighlighterPrivate() 0137 { 0138 delete spellchecker; 0139 delete languageFilter; 0140 delete tokenizer; 0141 } 0142 0143 Highlighter::Highlighter(QTextEdit *edit, const QColor &_col) 0144 : QSyntaxHighlighter(edit) 0145 , d(new HighlighterPrivate(this, _col)) 0146 { 0147 d->textEdit = edit; 0148 d->textEdit->installEventFilter(this); 0149 d->textEdit->viewport()->installEventFilter(this); 0150 } 0151 0152 Highlighter::Highlighter(QPlainTextEdit *edit, const QColor &col) 0153 : QSyntaxHighlighter(edit) 0154 , d(new HighlighterPrivate(this, col)) 0155 { 0156 d->plainTextEdit = edit; 0157 setDocument(d->plainTextEdit->document()); 0158 d->plainTextEdit->installEventFilter(this); 0159 d->plainTextEdit->viewport()->installEventFilter(this); 0160 } 0161 0162 Highlighter::~Highlighter() 0163 { 0164 delete d; 0165 } 0166 0167 bool Highlighter::spellCheckerFound() const 0168 { 0169 return d->spellCheckerFound; 0170 } 0171 0172 void Highlighter::slotRehighlight() 0173 { 0174 if (d->completeRehighlightRequired) { 0175 d->wordCount = 0; 0176 d->errorCount = 0; 0177 rehighlight(); 0178 } else { 0179 // rehighlight the current para only (undo/redo safe) 0180 QTextCursor cursor; 0181 if (d->textEdit) { 0182 cursor = d->textEdit->textCursor(); 0183 } else { 0184 cursor = d->plainTextEdit->textCursor(); 0185 } 0186 if (cursor.hasSelection()) { 0187 cursor.clearSelection(); 0188 } 0189 cursor.insertText(QString()); 0190 } 0191 // if (d->checksDone == d->checksRequested) 0192 // d->completeRehighlightRequired = false; 0193 QTimer::singleShot(0, this, SLOT(slotAutoDetection())); 0194 } 0195 0196 bool Highlighter::automatic() const 0197 { 0198 return d->automatic; 0199 } 0200 0201 bool Highlighter::autoDetectLanguageDisabled() const 0202 { 0203 return d->autoDetectLanguageDisabled; 0204 } 0205 0206 bool Highlighter::intraWordEditing() const 0207 { 0208 return d->intraWordEditing; 0209 } 0210 0211 void Highlighter::setIntraWordEditing(bool editing) 0212 { 0213 d->intraWordEditing = editing; 0214 } 0215 0216 void Highlighter::setAutomatic(bool automatic) 0217 { 0218 if (automatic == d->automatic) { 0219 return; 0220 } 0221 0222 d->automatic = automatic; 0223 if (d->automatic) { 0224 slotAutoDetection(); 0225 } 0226 } 0227 0228 void Highlighter::setAutoDetectLanguageDisabled(bool autoDetectDisabled) 0229 { 0230 d->autoDetectLanguageDisabled = autoDetectDisabled; 0231 } 0232 0233 void Highlighter::slotAutoDetection() 0234 { 0235 bool savedActive = d->active; 0236 0237 // don't disable just because 1 of 4 is misspelled. 0238 if (d->automatic && d->wordCount >= 10) { 0239 // tme = Too many errors 0240 /* clang-format off */ 0241 bool tme = (d->errorCount >= d->disableWordCount) 0242 && (d->errorCount * 100 >= d->disablePercentage * d->wordCount); 0243 /* clang-format on */ 0244 0245 if (d->active && tme) { 0246 d->active = false; 0247 } else if (!d->active && !tme) { 0248 d->active = true; 0249 } 0250 } 0251 0252 if (d->active != savedActive) { 0253 if (d->active) { 0254 Q_EMIT activeChanged(tr("As-you-type spell checking enabled.")); 0255 } else { 0256 qCDebug(SONNET_LOG_UI) << "Sonnet: Disabling spell checking, too many errors"; 0257 Q_EMIT activeChanged( 0258 tr("Too many misspelled words. " 0259 "As-you-type spell checking disabled.")); 0260 } 0261 0262 d->completeRehighlightRequired = true; 0263 d->rehighlightRequest->setInterval(100); 0264 d->rehighlightRequest->setSingleShot(true); 0265 } 0266 } 0267 0268 void Highlighter::setActive(bool active) 0269 { 0270 if (active == d->active) { 0271 return; 0272 } 0273 d->active = active; 0274 rehighlight(); 0275 0276 if (d->active) { 0277 Q_EMIT activeChanged(tr("As-you-type spell checking enabled.")); 0278 } else { 0279 Q_EMIT activeChanged(tr("As-you-type spell checking disabled.")); 0280 } 0281 } 0282 0283 bool Highlighter::isActive() const 0284 { 0285 return d->active; 0286 } 0287 0288 void Highlighter::contentsChange(int pos, int add, int rem) 0289 { 0290 // Invalidate the cache where the text has changed 0291 const QTextBlock &lastBlock = document()->findBlock(pos + add - rem); 0292 QTextBlock block = document()->findBlock(pos); 0293 do { 0294 LanguageCache *cache = dynamic_cast<LanguageCache *>(block.userData()); 0295 if (cache) { 0296 cache->invalidate(pos - block.position()); 0297 } 0298 block = block.next(); 0299 } while (block.isValid() && block < lastBlock); 0300 } 0301 0302 static bool hasNotEmptyText(const QString &text) 0303 { 0304 for (int i = 0; i < text.length(); ++i) { 0305 if (!text.at(i).isSpace()) { 0306 return true; 0307 } 0308 } 0309 return false; 0310 } 0311 0312 void Highlighter::highlightBlock(const QString &text) 0313 { 0314 if (!hasNotEmptyText(text) || !d->active || !d->spellCheckerFound) { 0315 return; 0316 } 0317 0318 if (!d->connected) { 0319 connect(document(), &QTextDocument::contentsChange, this, &Highlighter::contentsChange); 0320 d->connected = true; 0321 } 0322 QTextCursor cursor; 0323 if (d->textEdit) { 0324 cursor = d->textEdit->textCursor(); 0325 } else { 0326 cursor = d->plainTextEdit->textCursor(); 0327 } 0328 int index = cursor.position(); 0329 0330 const int lengthPosition = text.length() - 1; 0331 0332 if (index != lengthPosition // 0333 || (lengthPosition > 0 && !text[lengthPosition - 1].isLetter())) { 0334 d->languageFilter->setBuffer(text); 0335 0336 LanguageCache *cache = dynamic_cast<LanguageCache *>(currentBlockUserData()); 0337 if (!cache) { 0338 cache = new LanguageCache; 0339 setCurrentBlockUserData(cache); 0340 } 0341 0342 const bool autodetectLanguage = d->spellchecker->testAttribute(Speller::AutoDetectLanguage); 0343 while (d->languageFilter->hasNext()) { 0344 Token sentence = d->languageFilter->next(); 0345 if (autodetectLanguage && !d->autoDetectLanguageDisabled) { 0346 QString lang; 0347 QPair<int, int> spos = QPair<int, int>(sentence.position(), sentence.length()); 0348 // try cache first 0349 if (cache->languages.contains(spos)) { 0350 lang = cache->languages.value(spos); 0351 } else { 0352 lang = d->languageFilter->language(); 0353 if (!d->languageFilter->isSpellcheckable()) { 0354 lang.clear(); 0355 } 0356 cache->languages[spos] = lang; 0357 } 0358 if (lang.isEmpty()) { 0359 continue; 0360 } 0361 d->spellchecker->setLanguage(lang); 0362 } 0363 0364 d->tokenizer->setBuffer(sentence.toString()); 0365 int offset = sentence.position(); 0366 while (d->tokenizer->hasNext()) { 0367 Token word = d->tokenizer->next(); 0368 if (!d->tokenizer->isSpellcheckable()) { 0369 continue; 0370 } 0371 ++d->wordCount; 0372 if (d->spellchecker->isMisspelled(word.toString())) { 0373 ++d->errorCount; 0374 setMisspelled(word.position() + offset, word.length()); 0375 } else { 0376 unsetMisspelled(word.position() + offset, word.length()); 0377 } 0378 } 0379 } 0380 } 0381 // QTimer::singleShot( 0, this, SLOT(checkWords()) ); 0382 setCurrentBlockState(0); 0383 } 0384 0385 QString Highlighter::currentLanguage() const 0386 { 0387 return d->spellchecker->language(); 0388 } 0389 0390 void Highlighter::setCurrentLanguage(const QString &lang) 0391 { 0392 QString prevLang = d->spellchecker->language(); 0393 d->spellchecker->setLanguage(lang); 0394 d->spellCheckerFound = d->spellchecker->isValid(); 0395 if (!d->spellCheckerFound) { 0396 qCDebug(SONNET_LOG_UI) << "No dictionary for \"" << lang << "\" staying with the current language."; 0397 d->spellchecker->setLanguage(prevLang); 0398 return; 0399 } 0400 d->wordCount = 0; 0401 d->errorCount = 0; 0402 if (d->automatic || d->active) { 0403 d->rehighlightRequest->start(0); 0404 } 0405 } 0406 0407 void Highlighter::setMisspelled(int start, int count) 0408 { 0409 QTextCharFormat format; 0410 format.setFontUnderline(true); 0411 format.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); 0412 format.setUnderlineColor(d->spellColor); 0413 setFormat(start, count, format); 0414 } 0415 0416 void Highlighter::unsetMisspelled(int start, int count) 0417 { 0418 setFormat(start, count, QTextCharFormat()); 0419 } 0420 0421 bool Highlighter::eventFilter(QObject *o, QEvent *e) 0422 { 0423 if (!d->spellCheckerFound) { 0424 return false; 0425 } 0426 if ((o == d->textEdit || o == d->plainTextEdit) && (e->type() == QEvent::KeyPress)) { 0427 QKeyEvent *k = static_cast<QKeyEvent *>(e); 0428 // d->autoReady = true; 0429 if (d->rehighlightRequest->isActive()) { // try to stay out of the users way 0430 d->rehighlightRequest->start(500); 0431 } 0432 /* clang-format off */ 0433 if (k->key() == Qt::Key_Enter 0434 || k->key() == Qt::Key_Return 0435 || k->key() == Qt::Key_Up 0436 || k->key() == Qt::Key_Down 0437 || k->key() == Qt::Key_Left 0438 || k->key() == Qt::Key_Right 0439 || k->key() == Qt::Key_PageUp 0440 || k->key() == Qt::Key_PageDown 0441 || k->key() == Qt::Key_Home 0442 || k->key() == Qt::Key_End 0443 || (k->modifiers() == Qt::ControlModifier 0444 && (k->key() == Qt::Key_A 0445 || k->key() == Qt::Key_B 0446 || k->key() == Qt::Key_E 0447 || k->key() == Qt::Key_N 0448 || k->key() == Qt::Key_P))) { /* clang-format on */ 0449 if (intraWordEditing()) { 0450 setIntraWordEditing(false); 0451 d->completeRehighlightRequired = true; 0452 d->rehighlightRequest->setInterval(500); 0453 d->rehighlightRequest->setSingleShot(true); 0454 d->rehighlightRequest->start(); 0455 } 0456 } else { 0457 setIntraWordEditing(true); 0458 } 0459 if (k->key() == Qt::Key_Space // 0460 || k->key() == Qt::Key_Enter // 0461 || k->key() == Qt::Key_Return) { 0462 QTimer::singleShot(0, this, SLOT(slotAutoDetection())); 0463 } 0464 } else if (((d->textEdit && (o == d->textEdit->viewport())) // 0465 || (d->plainTextEdit && (o == d->plainTextEdit->viewport()))) // 0466 && (e->type() == QEvent::MouseButtonPress)) { 0467 // d->autoReady = true; 0468 if (intraWordEditing()) { 0469 setIntraWordEditing(false); 0470 d->completeRehighlightRequired = true; 0471 d->rehighlightRequest->setInterval(0); 0472 d->rehighlightRequest->setSingleShot(true); 0473 d->rehighlightRequest->start(); 0474 } 0475 } 0476 return false; 0477 } 0478 0479 void Highlighter::addWordToDictionary(const QString &word) 0480 { 0481 d->spellchecker->addToPersonal(word); 0482 } 0483 0484 void Highlighter::ignoreWord(const QString &word) 0485 { 0486 d->spellchecker->addToSession(word); 0487 } 0488 0489 QStringList Highlighter::suggestionsForWord(const QString &word, int max) 0490 { 0491 QStringList suggestions = d->spellchecker->suggest(word); 0492 if (max >= 0 && suggestions.count() > max) { 0493 suggestions = suggestions.mid(0, max); 0494 } 0495 return suggestions; 0496 } 0497 0498 QStringList Highlighter::suggestionsForWord(const QString &word, const QTextCursor &cursor, int max) 0499 { 0500 LanguageCache *cache = dynamic_cast<LanguageCache *>(cursor.block().userData()); 0501 if (cache) { 0502 const QString cachedLanguage = cache->languageAtPos(cursor.positionInBlock()); 0503 if (!cachedLanguage.isEmpty()) { 0504 d->spellchecker->setLanguage(cachedLanguage); 0505 } 0506 } 0507 QStringList suggestions = d->spellchecker->suggest(word); 0508 if (max >= 0 && suggestions.count() > max) { 0509 suggestions = suggestions.mid(0, max); 0510 } 0511 return suggestions; 0512 } 0513 0514 bool Highlighter::isWordMisspelled(const QString &word) 0515 { 0516 return d->spellchecker->isMisspelled(word); 0517 } 0518 0519 void Highlighter::setMisspelledColor(const QColor &color) 0520 { 0521 d->spellColor = color; 0522 } 0523 0524 bool Highlighter::checkerEnabledByDefault() const 0525 { 0526 return d->loader->settings()->checkerEnabledByDefault(); 0527 } 0528 0529 void Highlighter::setDocument(QTextDocument *document) 0530 { 0531 d->connected = false; 0532 QSyntaxHighlighter::setDocument(document); 0533 } 0534 } 0535 0536 #include "moc_highlighter.cpp"