Sonnet

highlighter.cpp
1 /*
2  * highlighter.cpp
3  *
4  * SPDX-FileCopyrightText: 2004 Zack Rusin <[email protected]>
5  * SPDX-FileCopyrightText: 2006 Laurent Montel <[email protected]>
6  * SPDX-FileCopyrightText: 2013 Martin Sandsmark <[email protected]>
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 
32 namespace Sonnet
33 {
34 // Cache of previously-determined languages (when using AutoDetectLanguage)
35 // There is one such cache per block (paragraph)
36 class LanguageCache : public QTextBlockUserData
37 {
38 public:
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...
61  QMapIterator<QPair<int, int>, QString> it(languages);
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 
72 class HighlighterPrivate
73 {
74 public:
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  connected = false;
86  wordCount = 0;
87  errorCount = 0;
88  intraWordEditing = false;
89  completeRehighlightRequired = false;
90  spellColor = spellColor.isValid() ? spellColor : Qt::red;
91  languageFilter = new LanguageFilter(new SentenceTokenizer());
92 
93  loader = Loader::openLoader();
94  loader->settings()->restore();
95 
96  spellchecker = new Sonnet::Speller();
97  spellCheckerFound = spellchecker->isValid();
98  rehighlightRequest = new QTimer(q);
99  q->connect(rehighlightRequest, &QTimer::timeout, q, &Highlighter::slotRehighlight);
100 
101  if (!spellCheckerFound) {
102  return;
103  }
104 
105  disablePercentage = loader->settings()->disablePercentageWordError();
106  disableWordCount = loader->settings()->disableWordErrorCount();
107 
108  completeRehighlightRequired = true;
109  rehighlightRequest->setInterval(0);
110  rehighlightRequest->setSingleShot(true);
111  rehighlightRequest->start();
112  }
113 
114  ~HighlighterPrivate();
115  WordTokenizer *tokenizer = nullptr;
116  LanguageFilter *languageFilter = nullptr;
117  Loader *loader = nullptr;
118  Speller *spellchecker = nullptr;
119  QTextEdit *textEdit = nullptr;
120  QPlainTextEdit *plainTextEdit = nullptr;
121  bool active;
122  bool automatic;
123  bool autoDetectLanguageDisabled;
124  bool completeRehighlightRequired;
125  bool intraWordEditing;
126  bool spellCheckerFound; // cached d->dict->isValid() value
127  bool connected;
128  int disablePercentage = 0;
129  int disableWordCount = 0;
130  int wordCount, errorCount;
131  QTimer *rehighlightRequest = nullptr;
132  QColor spellColor;
133  Highlighter *const q;
134 };
135 
136 HighlighterPrivate::~HighlighterPrivate()
137 {
138  delete spellchecker;
139  delete languageFilter;
140  delete tokenizer;
141 }
142 
143 Highlighter::Highlighter(QTextEdit *edit, const QColor &_col)
144  : QSyntaxHighlighter(edit)
145  , d(new HighlighterPrivate(this, _col))
146 {
147  d->textEdit = edit;
148  d->textEdit->installEventFilter(this);
149  d->textEdit->viewport()->installEventFilter(this);
150 }
151 
152 Highlighter::Highlighter(QPlainTextEdit *edit, const QColor &col)
153  : QSyntaxHighlighter(edit)
154  , d(new HighlighterPrivate(this, col))
155 {
156  d->plainTextEdit = edit;
157  setDocument(d->plainTextEdit->document());
158  d->plainTextEdit->installEventFilter(this);
159  d->plainTextEdit->viewport()->installEventFilter(this);
160 }
161 
162 Highlighter::~Highlighter()
163 {
164  delete d;
165 }
166 
168 {
169  return d->spellCheckerFound;
170 }
171 
173 {
174  if (d->completeRehighlightRequired) {
175  d->wordCount = 0;
176  d->errorCount = 0;
177  rehighlight();
178  } else {
179  // rehighlight the current para only (undo/redo safe)
180  QTextCursor cursor;
181  if (d->textEdit) {
182  cursor = d->textEdit->textCursor();
183  } else {
184  cursor = d->plainTextEdit->textCursor();
185  }
186  if (cursor.hasSelection()) {
187  cursor.clearSelection();
188  }
189  cursor.insertText(QString());
190  }
191  // if (d->checksDone == d->checksRequested)
192  // d->completeRehighlightRequired = false;
193  QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
194 }
195 
197 {
198  return d->automatic;
199 }
200 
202 {
203  return d->autoDetectLanguageDisabled;
204 }
205 
206 bool Highlighter::intraWordEditing() const
207 {
208  return d->intraWordEditing;
209 }
210 
211 void Highlighter::setIntraWordEditing(bool editing)
212 {
213  d->intraWordEditing = editing;
214 }
215 
216 void Highlighter::setAutomatic(bool automatic)
217 {
218  if (automatic == d->automatic) {
219  return;
220  }
221 
222  d->automatic = automatic;
223  if (d->automatic) {
225  }
226 }
227 
228 void Highlighter::setAutoDetectLanguageDisabled(bool autoDetectDisabled)
229 {
230  d->autoDetectLanguageDisabled = autoDetectDisabled;
231 }
232 
234 {
235  bool savedActive = d->active;
236 
237  // don't disable just because 1 of 4 is misspelled.
238  if (d->automatic && d->wordCount >= 10) {
239  // tme = Too many errors
240  /* clang-format off */
241  bool tme = (d->errorCount >= d->disableWordCount)
242  && (d->errorCount * 100 >= d->disablePercentage * d->wordCount);
243  /* clang-format on */
244 
245  if (d->active && tme) {
246  d->active = false;
247  } else if (!d->active && !tme) {
248  d->active = true;
249  }
250  }
251 
252  if (d->active != savedActive) {
253  if (d->active) {
254  Q_EMIT activeChanged(tr("As-you-type spell checking enabled."));
255  } else {
256  qCDebug(SONNET_LOG_UI) << "Sonnet: Disabling spell checking, too many errors";
258  tr("Too many misspelled words. "
259  "As-you-type spell checking disabled."));
260  }
261 
262  d->completeRehighlightRequired = true;
263  d->rehighlightRequest->setInterval(100);
264  d->rehighlightRequest->setSingleShot(true);
265  }
266 }
267 
268 void Highlighter::setActive(bool active)
269 {
270  if (active == d->active) {
271  return;
272  }
273  d->active = active;
274  rehighlight();
275 
276  if (d->active) {
277  Q_EMIT activeChanged(tr("As-you-type spell checking enabled."));
278  } else {
279  Q_EMIT activeChanged(tr("As-you-type spell checking disabled."));
280  }
281 }
282 
284 {
285  return d->active;
286 }
287 
288 void Highlighter::contentsChange(int pos, int add, int rem)
289 {
290  // Invalidate the cache where the text has changed
291  const QTextBlock &lastBlock = document()->findBlock(pos + add - rem);
292  QTextBlock block = document()->findBlock(pos);
293  do {
294  LanguageCache *cache = dynamic_cast<LanguageCache *>(block.userData());
295  if (cache) {
296  cache->invalidate(pos - block.position());
297  }
298  block = block.next();
299  } while (block.isValid() && block < lastBlock);
300 }
301 
302 static bool hasNotEmptyText(const QString &text)
303 {
304  for (int i = 0; i < text.length(); ++i) {
305  if (!text.at(i).isSpace()) {
306  return true;
307  }
308  }
309  return false;
310 }
311 
312 void Highlighter::highlightBlock(const QString &text)
313 {
314  if (!hasNotEmptyText(text) || !d->active || !d->spellCheckerFound) {
315  return;
316  }
317 
318  if (!d->connected) {
319  connect(document(), &QTextDocument::contentsChange, this, &Highlighter::contentsChange);
320  d->connected = true;
321  }
322  QTextCursor cursor;
323  if (d->textEdit) {
324  cursor = d->textEdit->textCursor();
325  } else {
326  cursor = d->plainTextEdit->textCursor();
327  }
328  int index = cursor.position();
329 
330  const int lengthPosition = text.length() - 1;
331 
332  if (index != lengthPosition //
333  || (lengthPosition > 0 && !text[lengthPosition - 1].isLetter())) {
334  d->languageFilter->setBuffer(text);
335 
336  LanguageCache *cache = dynamic_cast<LanguageCache *>(currentBlockUserData());
337  if (!cache) {
338  cache = new LanguageCache;
340  }
341 
342  const bool autodetectLanguage = d->spellchecker->testAttribute(Speller::AutoDetectLanguage);
343  while (d->languageFilter->hasNext()) {
344  Token sentence = d->languageFilter->next();
345  if (autodetectLanguage && !d->autoDetectLanguageDisabled) {
346  QString lang;
347  QPair<int, int> spos = QPair<int, int>(sentence.position(), sentence.length());
348  // try cache first
349  if (cache->languages.contains(spos)) {
350  lang = cache->languages.value(spos);
351  } else {
352  lang = d->languageFilter->language();
353  if (!d->languageFilter->isSpellcheckable()) {
354  lang.clear();
355  }
356  cache->languages[spos] = lang;
357  }
358  if (lang.isEmpty()) {
359  continue;
360  }
361  d->spellchecker->setLanguage(lang);
362  }
363 
364  d->tokenizer->setBuffer(sentence.toString());
365  int offset = sentence.position();
366  while (d->tokenizer->hasNext()) {
367  Token word = d->tokenizer->next();
368  if (!d->tokenizer->isSpellcheckable()) {
369  continue;
370  }
371  ++d->wordCount;
372  if (d->spellchecker->isMisspelled(word.toString())) {
373  ++d->errorCount;
374  setMisspelled(word.position() + offset, word.length());
375  } else {
376  unsetMisspelled(word.position() + offset, word.length());
377  }
378  }
379  }
380  }
381  // QTimer::singleShot( 0, this, SLOT(checkWords()) );
383 }
384 
386 {
387  return d->spellchecker->language();
388 }
389 
391 {
392  QString prevLang = d->spellchecker->language();
393  d->spellchecker->setLanguage(lang);
394  d->spellCheckerFound = d->spellchecker->isValid();
395  if (!d->spellCheckerFound) {
396  qCDebug(SONNET_LOG_UI) << "No dictionary for \"" << lang << "\" staying with the current language.";
397  d->spellchecker->setLanguage(prevLang);
398  return;
399  }
400  d->wordCount = 0;
401  d->errorCount = 0;
402  if (d->automatic || d->active) {
403  d->rehighlightRequest->start(0);
404  }
405 }
406 
407 void Highlighter::setMisspelled(int start, int count)
408 {
410  format.setFontUnderline(true);
412  format.setUnderlineColor(d->spellColor);
413  setFormat(start, count, format);
414 }
415 
416 void Highlighter::unsetMisspelled(int start, int count)
417 {
418  setFormat(start, count, QTextCharFormat());
419 }
420 
421 bool Highlighter::eventFilter(QObject *o, QEvent *e)
422 {
423  if (!d->spellCheckerFound) {
424  return false;
425  }
426  if ((o == d->textEdit || o == d->plainTextEdit) && (e->type() == QEvent::KeyPress)) {
427  QKeyEvent *k = static_cast<QKeyEvent *>(e);
428  // d->autoReady = true;
429  if (d->rehighlightRequest->isActive()) { // try to stay out of the users way
430  d->rehighlightRequest->start(500);
431  }
432  /* clang-format off */
433  if (k->key() == Qt::Key_Enter
434  || k->key() == Qt::Key_Return
435  || k->key() == Qt::Key_Up
436  || k->key() == Qt::Key_Down
437  || k->key() == Qt::Key_Left
438  || k->key() == Qt::Key_Right
439  || k->key() == Qt::Key_PageUp
440  || k->key() == Qt::Key_PageDown
441  || k->key() == Qt::Key_Home
442  || k->key() == Qt::Key_End
443  || (k->modifiers() == Qt::ControlModifier
444  && (k->key() == Qt::Key_A
445  || k->key() == Qt::Key_B
446  || k->key() == Qt::Key_E
447  || k->key() == Qt::Key_N
448  || k->key() == Qt::Key_P))) { /* clang-format on */
449  if (intraWordEditing()) {
450  setIntraWordEditing(false);
451  d->completeRehighlightRequired = true;
452  d->rehighlightRequest->setInterval(500);
453  d->rehighlightRequest->setSingleShot(true);
454  d->rehighlightRequest->start();
455  }
456  } else {
457  setIntraWordEditing(true);
458  }
459  if (k->key() == Qt::Key_Space //
460  || k->key() == Qt::Key_Enter //
461  || k->key() == Qt::Key_Return) {
462  QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
463  }
464  } else if (((d->textEdit && (o == d->textEdit->viewport())) //
465  || (d->plainTextEdit && (o == d->plainTextEdit->viewport()))) //
466  && (e->type() == QEvent::MouseButtonPress)) {
467  // d->autoReady = true;
468  if (intraWordEditing()) {
469  setIntraWordEditing(false);
470  d->completeRehighlightRequired = true;
471  d->rehighlightRequest->setInterval(0);
472  d->rehighlightRequest->setSingleShot(true);
473  d->rehighlightRequest->start();
474  }
475  }
476  return false;
477 }
478 
480 {
481  d->spellchecker->addToPersonal(word);
482 }
483 
485 {
486  d->spellchecker->addToSession(word);
487 }
488 
490 {
491  QStringList suggestions = d->spellchecker->suggest(word);
492  if (max >= 0 && suggestions.count() > max) {
493  suggestions = suggestions.mid(0, max);
494  }
495  return suggestions;
496 }
497 
499 {
500  LanguageCache *cache = dynamic_cast<LanguageCache *>(cursor.block().userData());
501  if (cache) {
502  const QString cachedLanguage = cache->languageAtPos(cursor.positionInBlock());
503  if (!cachedLanguage.isEmpty()) {
504  d->spellchecker->setLanguage(cachedLanguage);
505  }
506  }
507  QStringList suggestions = d->spellchecker->suggest(word);
508  if (max >= 0 && suggestions.count() > max) {
509  suggestions = suggestions.mid(0, max);
510  }
511  return suggestions;
512 }
513 
515 {
516  return d->spellchecker->isMisspelled(word);
517 }
518 
520 {
521  d->spellColor = color;
522 }
523 
525 {
526  return d->loader->settings()->checkerEnabledByDefault();
527 }
528 
530 {
531  d->connected = false;
533 }
534 }
bool isWordMisspelled(const QString &word)
Checks if a given word is marked as misspelled by the highlighter.
void setCurrentBlockUserData(QTextBlockUserData *data)
void setDocument(QTextDocument *doc)
void clearSelection()
Q_EMITQ_EMIT
int count(const T &value) const const
void setAutoDetectLanguageDisabled(bool autoDetectDisabled)
Sets whether to disable the automatic language detection.
void clear()
void setDocument(QTextDocument *document)
Set a new QTextDocument for this highlighter to operate on.
Q_SCRIPTABLE Q_NOREPLY void start()
void setFormat(int start, int count, const QTextCharFormat &format)
QTextBlock block() const const
class used for actual spell checking
Definition: speller.h:25
void setCurrentLanguage(const QString &language)
Set language to use for spell checking.
QTextBlock next() const const
bool isActive() const
Returns the state of spell checking.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
bool automatic() const
Returns the state of the automatic disabling of spell checking.
void setUnderlineStyle(QTextCharFormat::UnderlineStyle style)
Qt::KeyboardModifiers modifiers() const const
void setUnderlineColor(const QColor &color)
bool isSpace() const const
void slotAutoDetection()
Run auto detection, disabling spell checking if too many errors are found.
QList< T > mid(int pos, int length) const const
void timeout()
bool isEmpty() const const
int position() const const
QTextBlockUserData * currentBlockUserData() const const
void ignoreWord(const QString &word)
Ignores the given word.
int length() const const
QTextDocument * document() const const
void setAutomatic(bool automatic)
Sets whether to automatically disable spell checking if there's too many errors.
void slotRehighlight()
Force a new highlighting.
QTextBlockUserData * userData() const const
Key_Enter
void setMisspelledColor(const QColor &color)
Sets the color in which the highlighter underlines misspelled words.
bool checkerEnabledByDefault() const
Return true if checker is enabled by default.
QTextCharFormat format(int position) const const
The sonnet namespace.
QString currentLanguage() const
Returns the current language used for spell checking.
void setCurrentBlockState(int newState)
int positionInBlock() const const
bool autoDetectLanguageDisabled() const
Returns whether the automatic language detection is disabled, overriding the Sonnet settings.
int key() const const
QEvent::Type type() const const
QStringList suggestionsForWord(const QString &word, int max=10)
Returns a list of suggested replacements for the given misspelled word.
const QChar at(int position) const const
void insertText(const QString &text)
bool spellCheckerFound() const
Returns whether a spell checking backend with support for the currentLanguage was found.
bool hasSelection() const const
void setFontUnderline(bool underline)
QTextBlock findBlock(int pos) const const
void contentsChange(int position, int charsRemoved, int charsAdded)
ControlModifier
QString tr(const char *sourceText, const char *disambiguation, int n)
void activeChanged(const QString &description)
Emitted when as-you-type spell checking is enabled or disabled.
void setActive(bool active)
Enable/Disable spell checking.
int position() const const
bool isValid() const const
void addWordToDictionary(const QString &word)
Adds the given word permanently to the dictionary.
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sun Sep 25 2022 04:14:52 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.