6#include "spellcheckhighlighter.h"
7#include "guesslanguage.h"
8#include "languagefilter_p.h"
10#include "settingsimpl_p.h"
12#include "tokenizer_p.h"
14#include "quick_debug.h"
20#include <QTextBoundaryFinder>
21#include <QTextCharFormat>
35 QMap<QPair<int, int>, QString> languages;
38 void invalidate(
int pos)
40 QMutableMapIterator<QPair<int, int>, QString> it(languages);
42 while (it.hasPrevious()) {
44 if (it.key().first + it.key().second >= pos) {
52 QString languageAtPos(
int pos)
const
55 QMapIterator<QPair<int, int>, QString> it(languages);
56 while (it.hasNext()) {
58 if (it.key().first <= pos && it.key().first + it.key().second >= pos) {
66class HighlighterPrivate
69 HighlighterPrivate(SpellcheckHighlighter *qq)
72 tokenizer = std::make_unique<WordTokenizer>();
75 autoDetectLanguageDisabled =
false;
79 intraWordEditing =
false;
80 completeRehighlightRequired =
false;
81 spellColor = spellColor.isValid() ? spellColor :
Qt::red;
82 languageFilter = std::make_unique<LanguageFilter>(
new SentenceTokenizer());
84 loader = Loader::openLoader();
85 loader->settings()->restore();
87 spellchecker = std::make_unique<Speller>();
88 spellCheckerFound = spellchecker->isValid();
89 rehighlightRequest =
new QTimer(q);
92 if (!spellCheckerFound) {
96 disablePercentage = loader->settings()->disablePercentageWordError();
97 disableWordCount = loader->settings()->disableWordErrorCount();
99 completeRehighlightRequired =
true;
100 rehighlightRequest->setInterval(0);
101 rehighlightRequest->setSingleShot(
true);
102 rehighlightRequest->start();
105 errorFormat.setForeground(spellColor);
106 errorFormat.setUnderlineColor(spellColor);
109 selectedErrorFormat.setForeground(spellColor);
110 auto bg = spellColor;
112 selectedErrorFormat.setBackground(bg);
113 selectedErrorFormat.setUnderlineColor(spellColor);
116 quoteFormat.setForeground(QColor{
"#7f8c8d"});
119 ~HighlighterPrivate();
120 std::unique_ptr<WordTokenizer> tokenizer;
121 std::unique_ptr<LanguageFilter> languageFilter;
122 Loader *loader =
nullptr;
123 std::unique_ptr<Speller> spellchecker;
125 QTextCharFormat errorFormat;
126 QTextCharFormat selectedErrorFormat;
127 QTextCharFormat quoteFormat;
128 std::unique_ptr<Sonnet::GuessLanguage> languageGuesser;
129 QString selectedWord;
130 QQuickTextDocument *document =
nullptr;
131 int cursorPosition = 0;
132 int selectionStart = 0;
133 int selectionEnd = 0;
135 int autoCompleteBeginPosition = -1;
136 int autoCompleteEndPosition = -1;
137 int wordIsMisspelled =
false;
139 bool automatic =
false;
140 bool autoDetectLanguageDisabled =
false;
141 bool completeRehighlightRequired =
false;
142 bool intraWordEditing =
false;
143 bool spellCheckerFound =
false;
144 bool connected =
false;
145 int disablePercentage = 0;
146 int disableWordCount = 0;
149 QTimer *rehighlightRequest =
nullptr;
151 SpellcheckHighlighter *
const q;
154HighlighterPrivate::~HighlighterPrivate()
158SpellcheckHighlighter::SpellcheckHighlighter(
QObject *parent)
160 , d(new HighlighterPrivate(this))
164SpellcheckHighlighter::~SpellcheckHighlighter()
173 return d->spellCheckerFound;
178 if (d->completeRehighlightRequired) {
202 return d->autoDetectLanguageDisabled;
205bool SpellcheckHighlighter::intraWordEditing()
const
207 return d->intraWordEditing;
210void SpellcheckHighlighter::setIntraWordEditing(
bool editing)
212 d->intraWordEditing = editing;
215void SpellcheckHighlighter::setAutomatic(
bool automatic)
227void SpellcheckHighlighter::setAutoDetectLanguageDisabled(
bool autoDetectDisabled)
229 d->autoDetectLanguageDisabled = autoDetectDisabled;
234 bool savedActive = d->active;
237 if (d->automatic && d->wordCount >= 10) {
240 bool tme = (d->errorCount >= d->disableWordCount)
241 && (d->errorCount * 100 >= d->disablePercentage * d->wordCount);
244 if (d->active && tme) {
246 }
else if (!d->active && !tme) {
251 if (d->active != savedActive) {
253 Q_EMIT activeChanged(
tr(
"As-you-type spell checking enabled."));
255 qCDebug(SONNET_LOG_QUICK) <<
"Sonnet: Disabling spell checking, too many errors";
257 tr(
"Too many misspelled words. "
258 "As-you-type spell checking disabled."));
261 d->completeRehighlightRequired =
true;
262 d->rehighlightRequest->setInterval(100);
263 d->rehighlightRequest->setSingleShot(
true);
267void SpellcheckHighlighter::setActive(
bool active)
269 if (
active == d->active) {
277 Q_EMIT activeChanged(
tr(
"As-you-type spell checking enabled."));
279 Q_EMIT activeChanged(
tr(
"As-you-type spell checking disabled."));
288static bool hasNotEmptyText(
const QString &text)
290 for (
int i = 0; i < text.
length(); ++i) {
298void SpellcheckHighlighter::contentsChange(
int pos,
int add,
int rem)
301 const QTextBlock &lastBlock =
document()->findBlock(pos + add - rem);
302 QTextBlock block =
document()->findBlock(pos);
304 LanguageCache *cache =
dynamic_cast<LanguageCache *
>(block.
userData());
306 cache->invalidate(pos - block.
position());
308 block = block.
next();
309 }
while (block.
isValid() && block < lastBlock);
312void SpellcheckHighlighter::highlightBlock(
const QString &text)
314 if (!hasNotEmptyText(text) || !d->active || !d->spellCheckerFound) {
319 if (text.
isEmpty() || text.
at(0) == QLatin1Char(
'>')) {
328 QTextCursor cursor = textCursor();
329 const int index = cursor.
position() + 1;
331 const int lengthPosition = text.
length() - 1;
333 if (index != lengthPosition
334 || (lengthPosition > 0 && !text[lengthPosition - 1].isLetter())) {
335 d->languageFilter->setBuffer(text);
339 cache =
new LanguageCache;
343 const bool autodetectLanguage = d->spellchecker->testAttribute(Speller::AutoDetectLanguage);
344 while (d->languageFilter->hasNext()) {
345 Sonnet::Token sentence = d->languageFilter->next();
346 if (autodetectLanguage && !d->autoDetectLanguageDisabled) {
348 QPair<int, int> spos = QPair<int, int>(sentence.position(), sentence.length());
350 if (cache->languages.
contains(spos)) {
351 lang = cache->languages.
value(spos);
353 lang = d->languageFilter->language();
354 if (!d->languageFilter->isSpellcheckable()) {
357 cache->languages[spos] = lang;
362 d->spellchecker->setLanguage(lang);
365 d->tokenizer->setBuffer(sentence.toString());
366 int offset = sentence.position();
367 while (d->tokenizer->hasNext()) {
368 Sonnet::Token word = d->tokenizer->next();
369 if (!d->tokenizer->isSpellcheckable()) {
373 if (d->spellchecker->isMisspelled(word.toString())) {
375 if (word.position() + offset <= cursor.
position() && cursor.
position() <= word.position() + offset + word.length()) {
376 setMisspelledSelected(word.position() + offset, word.length());
378 setMisspelled(word.position() + offset, word.length());
381 unsetMisspelled(word.position() + offset, word.length());
392 if (!textDocument()) {
396 Q_EMIT changeCursorPosition(mousePosition, mousePosition);
416 d->selectedWord = d->selectedWord.right(d->selectedWord.size() - 1);
420 d->selectedWord.chop(1);
425 Q_EMIT wordUnderMouseChanged();
427 bool isMouseCursorInsideWord =
true;
429 && (d->selectedWord.length() > 1)) {
430 isMouseCursorInsideWord =
false;
435 d->wordIsMisspelled = isMouseCursorInsideWord && !d->selectedWord.isEmpty() && d->spellchecker->isMisspelled(d->selectedWord);
436 Q_EMIT wordIsMisspelledChanged();
438 if (!d->wordIsMisspelled || selectedWordClicked) {
442 LanguageCache *cache =
dynamic_cast<LanguageCache *
>(cursor.
block().userData());
445 if (!cachedLanguage.
isEmpty()) {
446 d->spellchecker->setLanguage(cachedLanguage);
464 QString prevLang = d->spellchecker->language();
465 d->spellchecker->setLanguage(lang);
466 d->spellCheckerFound = d->spellchecker->isValid();
467 if (!d->spellCheckerFound) {
468 qCDebug(SONNET_LOG_QUICK) <<
"No dictionary for \"" << lang <<
"\" staying with the current language.";
469 d->spellchecker->setLanguage(prevLang);
474 if (d->automatic || d->active) {
475 d->rehighlightRequest->start(0);
479void SpellcheckHighlighter::setMisspelled(
int start,
int count)
484void SpellcheckHighlighter::setMisspelledSelected(
int start,
int count)
489void SpellcheckHighlighter::unsetMisspelled(
int start,
int count)
496 d->spellchecker->addToPersonal(word);
502 d->spellchecker->addToSession(word);
508 QTextCursor textCursorUnderUserCursor(textDocument());
509 textCursorUnderUserCursor.
setPosition(at == -1 ? d->cursorPosition : at);
512 QTextCursor wordSelectCursor(textCursorUnderUserCursor);
521 selectedWord = selectedWord.right(selectedWord.size() - 1);
525 selectedWord.chop(1);
545 d->document->parent()->removeEventFilter(
this);
546 d->document->textDocument()->disconnect(
this);
549 document->parent()->installEventFilter(
this);
556 d->connected =
false;
562 return d->cursorPosition;
565void SpellcheckHighlighter::setCursorPosition(
int position)
567 if (position == d->cursorPosition) {
571 d->cursorPosition = position;
572 d->rehighlightRequest->start(0);
573 Q_EMIT cursorPositionChanged();
578 return d->selectionStart;
581void SpellcheckHighlighter::setSelectionStart(
int position)
583 if (position == d->selectionStart) {
587 d->selectionStart = position;
588 Q_EMIT selectionStartChanged();
593 return d->selectionEnd;
596void SpellcheckHighlighter::setSelectionEnd(
int position)
598 if (position == d->selectionEnd) {
602 d->selectionEnd = position;
603 Q_EMIT selectionEndChanged();
606QTextCursor SpellcheckHighlighter::textCursor()
const
608 QTextDocument *doc = textDocument();
610 return QTextCursor();
613 QTextCursor cursor(doc);
614 if (d->selectionStart != d->selectionEnd) {
629 return d->document->textDocument();
634 return d->wordIsMisspelled;
639 return d->selectedWord;
644 return d->spellColor;
647void SpellcheckHighlighter::setMisspelledColor(
const QColor &color)
649 if (color == d->spellColor) {
652 d->spellColor = color;
653 Q_EMIT misspelledColorChanged();
658 return d->spellchecker->isMisspelled(word);
661bool SpellcheckHighlighter::eventFilter(
QObject *o,
QEvent *e)
663 if (!d->spellCheckerFound) {
667 QKeyEvent *k =
static_cast<QKeyEvent *
>(e);
674 if (intraWordEditing()) {
675 setIntraWordEditing(
false);
676 d->completeRehighlightRequired =
true;
677 d->rehighlightRequest->setInterval(500);
678 d->rehighlightRequest->setSingleShot(
true);
679 d->rehighlightRequest->start();
682 setIntraWordEditing(
true);
690 if (intraWordEditing()) {
691 setIntraWordEditing(
false);
692 d->completeRehighlightRequired =
true;
693 d->rehighlightRequest->setInterval(0);
694 d->rehighlightRequest->setSingleShot(
true);
695 d->rehighlightRequest->start();
701#include "moc_spellcheckhighlighter.cpp"
void slotRehighlight()
Force a new highlighting.
bool wordIsMisspelled
This property holds whether the current word under the mouse is misspelled.
Q_INVOKABLE void ignoreWord(const QString &word)
Ignores the given word.
int selectionStart
This property holds the start of the selection.
void setCurrentLanguage(const QString &language)
Set language to use for spell checking.
QString currentLanguage
This property holds the current language used for spell checking.
QString wordUnderMouse
This property holds the current word under the mouse.
QML_ELEMENTQQuickTextDocument * document
This property holds the underneath document from a QML TextEdit.
void setDocument(QTextDocument *document)
Set a new QTextDocument for this highlighter to operate on.
Q_INVOKABLE void replaceWord(const QString &word, int at=-1)
Replace word at the current cursor position, or.
int selectionEnd
This property holds the end of the selection.
int cursorPosition
This property holds the current cursor position.
bool autoDetectLanguageDisabled
This property holds whether the automatic language detection is disabled overriding the Sonnet global...
bool active
This property holds whether spell checking is enabled.
Q_INVOKABLE QStringList suggestions(int position, int max=5)
Returns a list of suggested replacements for the given misspelled word.
void slotAutoDetection()
Run auto detection, disabling spell checking if too many errors are found.
QColor misspelledColor
This property holds the spell color.
Q_INVOKABLE bool isWordMisspelled(const QString &word)
Checks if a given word is marked as misspelled by the highlighter.
bool automatic
This property holds whether spell checking is automatically disabled if there's too many errors.
bool spellCheckerFound
This property holds whether a spell checking backend with support for the currentLanguage was found.
Q_INVOKABLE void addWordToDictionary(const QString &word)
Adds the given word permanently to the dictionary.
Q_SCRIPTABLE QString start(QString train="")
Q_SCRIPTABLE Q_NOREPLY void start()
bool isSpace(char32_t ucs4)
Qt::KeyboardModifiers modifiers() const const
bool contains(const Key &key) const const
T value(const Key &key, const T &defaultValue) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QString tr(const char *sourceText, const char *disambiguation, int n)
const QChar at(qsizetype position) const const
bool isEmpty() const const
qsizetype length() const const
QTextBlockUserData * currentBlockUserData() const const
void setCurrentBlockState(int newState)
void setCurrentBlockUserData(QTextBlockUserData *data)
void setDocument(QTextDocument *doc)
bool isValid() const const
QTextBlock next() const const
int position() const const
QTextBlockUserData * userData() const const
QTextBlock block() const const
bool hasSelection() const const
void insertText(const QString &text)
bool movePosition(MoveOperation operation, MoveMode mode, int n)
int position() const const
int positionInBlock() const const
void select(SelectionType selection)
QString selectedText() const const
int selectionEnd() const const
int selectionStart() const const
void setPosition(int pos, MoveMode m)
void contentsChange(int position, int charsRemoved, int charsAdded)