| 1 | /* |
| 2 | * backgroundchecker.cpp |
| 3 | * |
| 4 | * SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org> |
| 5 | * SPDX-FileCopyrightText: 2009 Jakub Stachowski <qbast@go2.pl> |
| 6 | * |
| 7 | * SPDX-License-Identifier: LGPL-2.1-or-later |
| 8 | */ |
| 9 | #include "backgroundchecker.h" |
| 10 | #include "backgroundchecker_p.h" |
| 11 | |
| 12 | #include "core_debug.h" |
| 13 | |
| 14 | using namespace Sonnet; |
| 15 | |
| 16 | void BackgroundCheckerPrivate::start() |
| 17 | { |
| 18 | sentenceOffset = -1; |
| 19 | continueChecking(); |
| 20 | } |
| 21 | |
| 22 | void BackgroundCheckerPrivate::continueChecking() |
| 23 | { |
| 24 | metaObject()->invokeMethod(obj: this, member: "checkNext" , c: Qt::QueuedConnection); |
| 25 | } |
| 26 | |
| 27 | void BackgroundCheckerPrivate::checkNext() |
| 28 | { |
| 29 | do { |
| 30 | // go over current sentence |
| 31 | while (sentenceOffset != -1 && words.hasNext()) { |
| 32 | Token word = words.next(); |
| 33 | if (!words.isSpellcheckable()) { |
| 34 | continue; |
| 35 | } |
| 36 | |
| 37 | // ok, this is valid word, do something |
| 38 | if (currentDict.isMisspelled(word: word.toString())) { |
| 39 | lastMisspelled = word; |
| 40 | Q_EMIT misspelling(word.toString(), word.position() + sentenceOffset); |
| 41 | return; |
| 42 | } |
| 43 | } |
| 44 | // current sentence done, grab next suitable |
| 45 | |
| 46 | sentenceOffset = -1; |
| 47 | const bool autodetectLanguage = currentDict.testAttribute(attr: Speller::AutoDetectLanguage); |
| 48 | const bool ignoreUpperCase = !currentDict.testAttribute(attr: Speller::CheckUppercase); |
| 49 | while (mainTokenizer.hasNext()) { |
| 50 | Token sentence = mainTokenizer.next(); |
| 51 | if (autodetectLanguage && !autoDetectLanguageDisabled) { |
| 52 | if (!mainTokenizer.isSpellcheckable()) { |
| 53 | continue; |
| 54 | } |
| 55 | // FIXME: find best from family en -> en_US, en_GB, ... ? |
| 56 | currentDict.setLanguage(mainTokenizer.language()); |
| 57 | } |
| 58 | sentenceOffset = sentence.position(); |
| 59 | words.setBuffer(sentence.toString()); |
| 60 | words.setIgnoreUppercase(ignoreUpperCase); |
| 61 | break; |
| 62 | } |
| 63 | } while (sentenceOffset != -1); |
| 64 | Q_EMIT done(); |
| 65 | } |
| 66 | |
| 67 | BackgroundChecker::BackgroundChecker(QObject *parent) |
| 68 | : QObject(parent) |
| 69 | , d(new BackgroundCheckerPrivate) |
| 70 | { |
| 71 | connect(sender: d.get(), signal: &BackgroundCheckerPrivate::misspelling, context: this, slot: &BackgroundChecker::misspelling); |
| 72 | connect(sender: d.get(), signal: &BackgroundCheckerPrivate::done, context: this, slot: &BackgroundChecker::slotEngineDone); |
| 73 | } |
| 74 | |
| 75 | BackgroundChecker::BackgroundChecker(const Speller &speller, QObject *parent) |
| 76 | : QObject(parent) |
| 77 | , d(new BackgroundCheckerPrivate) |
| 78 | { |
| 79 | d->currentDict = speller; |
| 80 | connect(sender: d.get(), signal: &BackgroundCheckerPrivate::misspelling, context: this, slot: &BackgroundChecker::misspelling); |
| 81 | connect(sender: d.get(), signal: &BackgroundCheckerPrivate::done, context: this, slot: &BackgroundChecker::slotEngineDone); |
| 82 | } |
| 83 | |
| 84 | BackgroundChecker::~BackgroundChecker() = default; |
| 85 | |
| 86 | void BackgroundChecker::setText(const QString &text) |
| 87 | { |
| 88 | d->mainTokenizer.setBuffer(text); |
| 89 | d->start(); |
| 90 | } |
| 91 | |
| 92 | void BackgroundChecker::start() |
| 93 | { |
| 94 | // ## what if d->currentText.isEmpty()? |
| 95 | |
| 96 | // TODO: carry state from last buffer |
| 97 | d->mainTokenizer.setBuffer(fetchMoreText()); |
| 98 | d->start(); |
| 99 | } |
| 100 | |
| 101 | void BackgroundChecker::stop() |
| 102 | { |
| 103 | // d->stop(); |
| 104 | } |
| 105 | |
| 106 | QString BackgroundChecker::fetchMoreText() |
| 107 | { |
| 108 | return QString(); |
| 109 | } |
| 110 | |
| 111 | void BackgroundChecker::finishedCurrentFeed() |
| 112 | { |
| 113 | } |
| 114 | |
| 115 | bool BackgroundChecker::autoDetectLanguageDisabled() const |
| 116 | { |
| 117 | return d->autoDetectLanguageDisabled; |
| 118 | } |
| 119 | |
| 120 | void BackgroundChecker::setAutoDetectLanguageDisabled(bool autoDetectDisabled) |
| 121 | { |
| 122 | d->autoDetectLanguageDisabled = autoDetectDisabled; |
| 123 | } |
| 124 | |
| 125 | void BackgroundChecker::setSpeller(const Speller &speller) |
| 126 | { |
| 127 | d->currentDict = speller; |
| 128 | } |
| 129 | |
| 130 | Speller BackgroundChecker::speller() const |
| 131 | { |
| 132 | return d->currentDict; |
| 133 | } |
| 134 | |
| 135 | bool BackgroundChecker::checkWord(const QString &word) |
| 136 | { |
| 137 | return d->currentDict.isCorrect(word); |
| 138 | } |
| 139 | |
| 140 | bool BackgroundChecker::addWordToPersonal(const QString &word) |
| 141 | { |
| 142 | return d->currentDict.addToPersonal(word); |
| 143 | } |
| 144 | |
| 145 | bool BackgroundChecker::addWordToSession(const QString &word) |
| 146 | { |
| 147 | return d->currentDict.addToSession(word); |
| 148 | } |
| 149 | |
| 150 | QStringList BackgroundChecker::suggest(const QString &word) const |
| 151 | { |
| 152 | return d->currentDict.suggest(word); |
| 153 | } |
| 154 | |
| 155 | void BackgroundChecker::changeLanguage(const QString &lang) |
| 156 | { |
| 157 | // this sets language only for current sentence |
| 158 | d->currentDict.setLanguage(lang); |
| 159 | } |
| 160 | |
| 161 | void BackgroundChecker::continueChecking() |
| 162 | { |
| 163 | d->continueChecking(); |
| 164 | } |
| 165 | |
| 166 | void BackgroundChecker::slotEngineDone() |
| 167 | { |
| 168 | finishedCurrentFeed(); |
| 169 | const QString currentText = fetchMoreText(); |
| 170 | |
| 171 | if (currentText.isNull()) { |
| 172 | Q_EMIT done(); |
| 173 | } else { |
| 174 | d->mainTokenizer.setBuffer(currentText); |
| 175 | d->start(); |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | QString BackgroundChecker::text() const |
| 180 | { |
| 181 | return d->mainTokenizer.buffer(); |
| 182 | } |
| 183 | |
| 184 | QString BackgroundChecker::currentContext() const |
| 185 | { |
| 186 | int len = 60; |
| 187 | // we don't want the expression underneath casted to an unsigned int |
| 188 | // which would cause it to always evaluate to false |
| 189 | int currentPosition = d->lastMisspelled.position() + d->sentenceOffset; |
| 190 | bool begin = ((currentPosition - len / 2) <= 0) ? true : false; |
| 191 | |
| 192 | QString buffer = d->mainTokenizer.buffer(); |
| 193 | buffer.replace(i: currentPosition, len: d->lastMisspelled.length(), QStringLiteral("<b>%1</b>" ).arg(a: d->lastMisspelled.toString())); |
| 194 | |
| 195 | QString context; |
| 196 | if (begin) { |
| 197 | context = QStringLiteral("%1..." ).arg(a: buffer.mid(position: 0, n: len)); |
| 198 | } else { |
| 199 | context = QStringLiteral("...%1..." ).arg(a: buffer.mid(position: currentPosition - 20, n: len)); |
| 200 | } |
| 201 | |
| 202 | context.replace(before: QLatin1Char('\n'), after: QLatin1Char(' ')); |
| 203 | |
| 204 | return context; |
| 205 | } |
| 206 | |
| 207 | void Sonnet::BackgroundChecker::replace(int start, const QString &oldText, const QString &newText) |
| 208 | { |
| 209 | // FIXME: here we assume that replacement is in current fragment. So 'words' has |
| 210 | // to be adjusted and sentenceOffset does not |
| 211 | d->words.replace(position: start - (d->sentenceOffset), len: oldText.length(), newWord: newText); |
| 212 | d->mainTokenizer.replace(position: start, len: oldText.length(), newWord: newText); |
| 213 | } |
| 214 | |
| 215 | #include "moc_backgroundchecker.cpp" |
| 216 | #include "moc_backgroundchecker_p.cpp" |
| 217 | |