| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <michel.ludwig@kdemail.net> |
| 3 | SPDX-FileCopyrightText: 2008 Mirko Stocker <me@misto.ch> |
| 4 | SPDX-FileCopyrightText: 2004-2005 Anders Lund <anders@alweb.dk> |
| 5 | SPDX-FileCopyrightText: 2002 John Firebaugh <jfirebaugh@kde.org> |
| 6 | SPDX-FileCopyrightText: 2001-2004 Christoph Cullmann <cullmann@kde.org> |
| 7 | SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org> |
| 8 | SPDX-FileCopyrightText: 1999 Jochen Wilhelmy <digisnap@cs.tu-berlin.de> |
| 9 | |
| 10 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 11 | */ |
| 12 | |
| 13 | #include "spellcheckdialog.h" |
| 14 | |
| 15 | #include "katedocument.h" |
| 16 | #include "kateglobal.h" |
| 17 | #include "kateview.h" |
| 18 | #include "spellcheck/spellcheck.h" |
| 19 | #include "spellcheck/spellcheckbar.h" |
| 20 | |
| 21 | #include <KActionCollection> |
| 22 | #include <KLocalizedString> |
| 23 | #include <KStandardActions> |
| 24 | |
| 25 | #include <sonnet/backgroundchecker.h> |
| 26 | #include <sonnet/speller.h> |
| 27 | |
| 28 | KateSpellCheckDialog::KateSpellCheckDialog(KTextEditor::ViewPrivate *view) |
| 29 | : QObject(view) |
| 30 | , m_view(view) |
| 31 | , m_speller(nullptr) |
| 32 | , m_backgroundChecker(nullptr) |
| 33 | , m_sonnetDialog(nullptr) |
| 34 | , m_globalSpellCheckRange(nullptr) |
| 35 | , m_spellCheckCancelledByUser(false) |
| 36 | { |
| 37 | } |
| 38 | |
| 39 | KateSpellCheckDialog::~KateSpellCheckDialog() |
| 40 | { |
| 41 | delete m_globalSpellCheckRange; |
| 42 | delete m_sonnetDialog; |
| 43 | delete m_backgroundChecker; |
| 44 | delete m_speller; |
| 45 | } |
| 46 | |
| 47 | void KateSpellCheckDialog::createActions(KActionCollection *ac) |
| 48 | { |
| 49 | ac->addAction(actionType: KStandardActions::Spelling, receiver: this, slot: qOverload<>(&KateSpellCheckDialog::spellcheck)); |
| 50 | |
| 51 | auto *a = new QAction(i18nc("@action" , "Spelling (from Cursor)…" ), this); |
| 52 | ac->addAction(QStringLiteral("tools_spelling_from_cursor" ), action: a); |
| 53 | a->setIcon(QIcon::fromTheme(QStringLiteral("tools-check-spelling" ))); |
| 54 | a->setWhatsThis(i18n("Check the document's spelling from the cursor and forward" )); |
| 55 | connect(sender: a, signal: &QAction::triggered, context: this, slot: &KateSpellCheckDialog::spellcheckFromCursor); |
| 56 | } |
| 57 | |
| 58 | void KateSpellCheckDialog::spellcheckFromCursor() |
| 59 | { |
| 60 | if (m_view->selection()) { |
| 61 | spellcheckSelection(); |
| 62 | } else { |
| 63 | spellcheck(from: m_view->cursorPosition()); |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | void KateSpellCheckDialog::spellcheckSelection() |
| 68 | { |
| 69 | spellcheck(from: m_view->selectionRange().start(), to: m_view->selectionRange().end()); |
| 70 | } |
| 71 | |
| 72 | void KateSpellCheckDialog::spellcheck() |
| 73 | { |
| 74 | if (m_view->selection()) { |
| 75 | spellcheckSelection(); |
| 76 | } else { |
| 77 | spellcheck(from: KTextEditor::Cursor(0, 0)); |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | void KateSpellCheckDialog::spellcheck(const KTextEditor::Cursor from, const KTextEditor::Cursor to) |
| 82 | { |
| 83 | KTextEditor::Cursor start = from; |
| 84 | KTextEditor::Cursor end = to; |
| 85 | |
| 86 | if (end.line() == 0 && end.column() == 0) { |
| 87 | end = m_view->doc()->documentEnd(); |
| 88 | } |
| 89 | |
| 90 | if (!m_speller) { |
| 91 | m_speller = new Sonnet::Speller(); |
| 92 | } |
| 93 | m_speller->restore(); |
| 94 | |
| 95 | if (!m_backgroundChecker) { |
| 96 | m_backgroundChecker = new Sonnet::BackgroundChecker(*m_speller); |
| 97 | } |
| 98 | |
| 99 | if (!m_sonnetDialog) { |
| 100 | m_sonnetDialog = new SpellCheckBar(m_backgroundChecker, m_view); |
| 101 | m_sonnetDialog->showProgressDialog(timeout: 200); |
| 102 | m_sonnetDialog->showSpellCheckCompletionMessage(); |
| 103 | m_sonnetDialog->setSpellCheckContinuedAfterReplacement(false); |
| 104 | |
| 105 | connect(sender: m_sonnetDialog, signal: &SpellCheckBar::done, context: this, slot: &KateSpellCheckDialog::installNextSpellCheckRange); |
| 106 | |
| 107 | connect(sender: m_sonnetDialog, signal: &SpellCheckBar::replace, context: this, slot: &KateSpellCheckDialog::corrected); |
| 108 | |
| 109 | connect(sender: m_sonnetDialog, signal: &SpellCheckBar::misspelling, context: this, slot: &KateSpellCheckDialog::misspelling); |
| 110 | |
| 111 | connect(sender: m_sonnetDialog, signal: &SpellCheckBar::cancel, context: this, slot: &KateSpellCheckDialog::cancelClicked); |
| 112 | |
| 113 | connect(sender: m_sonnetDialog, signal: &SpellCheckBar::destroyed, context: this, slot: &KateSpellCheckDialog::objectDestroyed); |
| 114 | |
| 115 | connect(sender: m_sonnetDialog, signal: &SpellCheckBar::languageChanged, context: this, slot: &KateSpellCheckDialog::languageChanged); |
| 116 | } |
| 117 | |
| 118 | m_view->bottomViewBar()->addBarWidget(newBarWidget: m_sonnetDialog); |
| 119 | |
| 120 | m_userSpellCheckLanguage.clear(); |
| 121 | m_previousGivenSpellCheckLanguage.clear(); |
| 122 | delete m_globalSpellCheckRange; |
| 123 | // we expand to handle the situation when the last word in the range is replace by a new one |
| 124 | m_globalSpellCheckRange = |
| 125 | m_view->doc()->newMovingRange(range: KTextEditor::Range(start, end), insertBehaviors: KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); |
| 126 | m_spellCheckCancelledByUser = false; |
| 127 | performSpellCheck(range: *m_globalSpellCheckRange); |
| 128 | } |
| 129 | |
| 130 | KTextEditor::Cursor KateSpellCheckDialog::locatePosition(int pos) |
| 131 | { |
| 132 | uint remains; |
| 133 | |
| 134 | while (m_spellLastPos < (uint)pos) { |
| 135 | remains = pos - m_spellLastPos; |
| 136 | uint l = m_view->doc()->lineLength(line: m_spellPosCursor.line()) - m_spellPosCursor.column(); |
| 137 | if (l > remains) { |
| 138 | m_spellPosCursor.setColumn(m_spellPosCursor.column() + remains); |
| 139 | m_spellLastPos = pos; |
| 140 | } else { |
| 141 | m_spellPosCursor.setLine(m_spellPosCursor.line() + 1); |
| 142 | m_spellPosCursor.setColumn(0); |
| 143 | m_spellLastPos += l + 1; |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | return m_spellPosCursor; |
| 148 | } |
| 149 | |
| 150 | void KateSpellCheckDialog::misspelling(const QString &word, int pos) |
| 151 | { |
| 152 | KTextEditor::Cursor cursor; |
| 153 | int length; |
| 154 | int origPos = m_view->doc()->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos); |
| 155 | cursor = locatePosition(pos: origPos); |
| 156 | length = m_view->doc()->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos: pos + word.length()) - origPos; |
| 157 | |
| 158 | m_view->setCursorPositionInternal(position: cursor, tabwidth: 1); |
| 159 | m_view->setSelection(KTextEditor::Range(cursor, length)); |
| 160 | } |
| 161 | |
| 162 | void KateSpellCheckDialog::corrected(const QString &word, int pos, const QString &newWord) |
| 163 | { |
| 164 | int origPos = m_view->doc()->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos); |
| 165 | |
| 166 | int length = m_view->doc()->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos: pos + word.length()) - origPos; |
| 167 | |
| 168 | KTextEditor::Cursor replacementStartCursor = locatePosition(pos: origPos); |
| 169 | KTextEditor::Range replacementRange = KTextEditor::Range(replacementStartCursor, length); |
| 170 | KTextEditor::DocumentPrivate *doc = m_view->doc(); |
| 171 | KTextEditor::EditorPrivate::self()->spellCheckManager()->replaceCharactersEncodedIfNecessary(newWord, doc, replacementRange); |
| 172 | |
| 173 | // we have to be careful here: due to static word wrapping the text might change in addition to simply |
| 174 | // the misspelled word being replaced, i.e. new line breaks might be inserted as well. As such, the text |
| 175 | // in the 'Sonnet::Dialog' might be eventually out of sync with the visible text. Therefore, we 'restart' |
| 176 | // spell checking from the current position. |
| 177 | performSpellCheck(range: KTextEditor::Range(replacementStartCursor, m_globalSpellCheckRange->end())); |
| 178 | } |
| 179 | |
| 180 | void KateSpellCheckDialog::performSpellCheck(KTextEditor::Range range) |
| 181 | { |
| 182 | if (range.isEmpty()) { |
| 183 | spellCheckDone(); |
| 184 | m_sonnetDialog->closed(); |
| 185 | return; |
| 186 | } |
| 187 | m_languagesInSpellCheckRange = KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckLanguageRanges(doc: m_view->doc(), range); |
| 188 | m_currentLanguageRangeIterator = m_languagesInSpellCheckRange.begin(); |
| 189 | m_currentSpellCheckRange = KTextEditor::Range::invalid(); |
| 190 | installNextSpellCheckRange(); |
| 191 | // first check if there is really something to spell check |
| 192 | if (m_currentSpellCheckRange.isValid()) { |
| 193 | m_view->bottomViewBar()->showBarWidget(barWidget: m_sonnetDialog); |
| 194 | m_sonnetDialog->show(); |
| 195 | m_sonnetDialog->setFocus(); |
| 196 | } else { |
| 197 | m_sonnetDialog->closed(); |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | void KateSpellCheckDialog::installNextSpellCheckRange() |
| 202 | { |
| 203 | if (m_spellCheckCancelledByUser || m_currentLanguageRangeIterator == m_languagesInSpellCheckRange.end()) { |
| 204 | spellCheckDone(); |
| 205 | return; |
| 206 | } |
| 207 | KateSpellCheckManager *spellCheckManager = KTextEditor::EditorPrivate::self()->spellCheckManager(); |
| 208 | KTextEditor::Cursor = (m_currentSpellCheckRange.isValid() ? m_currentSpellCheckRange.end() : KTextEditor::Cursor::invalid()); |
| 209 | m_currentSpellCheckRange = KTextEditor::Range::invalid(); |
| 210 | m_currentDecToEncOffsetList.clear(); |
| 211 | QList<QPair<KTextEditor::Range, QString>> rangeDictionaryPairList; |
| 212 | while (m_currentLanguageRangeIterator != m_languagesInSpellCheckRange.end()) { |
| 213 | KTextEditor::Range currentLanguageRange = (*m_currentLanguageRangeIterator).first; |
| 214 | const QString &dictionary = (*m_currentLanguageRangeIterator).second; |
| 215 | KTextEditor::Range languageSubRange = |
| 216 | (nextRangeBegin.isValid() ? KTextEditor::Range(nextRangeBegin, currentLanguageRange.end()) : currentLanguageRange); |
| 217 | rangeDictionaryPairList = spellCheckManager->spellCheckWrtHighlightingRanges(doc: m_view->doc(), range: languageSubRange, dictionary, singleLine: false, returnSingleRange: true); |
| 218 | Q_ASSERT(rangeDictionaryPairList.size() <= 1); |
| 219 | if (rangeDictionaryPairList.size() == 0) { |
| 220 | ++m_currentLanguageRangeIterator; |
| 221 | if (m_currentLanguageRangeIterator != m_languagesInSpellCheckRange.end()) { |
| 222 | nextRangeBegin = (*m_currentLanguageRangeIterator).first.start(); |
| 223 | } |
| 224 | } else { |
| 225 | m_currentSpellCheckRange = rangeDictionaryPairList.first().first; |
| 226 | QString dictionary = rangeDictionaryPairList.first().second; |
| 227 | const bool languageChanged = (dictionary != m_previousGivenSpellCheckLanguage); |
| 228 | m_previousGivenSpellCheckLanguage = dictionary; |
| 229 | |
| 230 | // if there was no change of dictionary stemming from the document language ranges and |
| 231 | // the user has set a dictionary in the dialog, we use that one |
| 232 | if (!languageChanged && !m_userSpellCheckLanguage.isEmpty()) { |
| 233 | dictionary = m_userSpellCheckLanguage; |
| 234 | } |
| 235 | // we only allow the user to override the preset dictionary within a language range |
| 236 | // given by the document |
| 237 | else if (languageChanged) { |
| 238 | m_userSpellCheckLanguage.clear(); |
| 239 | } |
| 240 | |
| 241 | m_spellPosCursor = m_currentSpellCheckRange.start(); |
| 242 | m_spellLastPos = 0; |
| 243 | |
| 244 | m_currentDecToEncOffsetList.clear(); |
| 245 | KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList; |
| 246 | QString text = m_view->doc()->decodeCharacters(range: m_currentSpellCheckRange, decToEncOffsetList&: m_currentDecToEncOffsetList, encToDecOffsetList); |
| 247 | // ensure that no empty string is passed on to Sonnet as this can lead to a crash |
| 248 | // (bug 228789) |
| 249 | if (text.isEmpty()) { |
| 250 | nextRangeBegin = m_currentSpellCheckRange.end(); |
| 251 | continue; |
| 252 | } |
| 253 | |
| 254 | if (m_speller->language() != dictionary) { |
| 255 | m_speller->setLanguage(dictionary); |
| 256 | m_backgroundChecker->setSpeller(*m_speller); |
| 257 | } |
| 258 | |
| 259 | m_sonnetDialog->setBuffer(text); |
| 260 | break; |
| 261 | } |
| 262 | } |
| 263 | if (m_currentLanguageRangeIterator == m_languagesInSpellCheckRange.end()) { |
| 264 | spellCheckDone(); |
| 265 | return; |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | void KateSpellCheckDialog::cancelClicked() |
| 270 | { |
| 271 | m_spellCheckCancelledByUser = true; |
| 272 | spellCheckDone(); |
| 273 | } |
| 274 | |
| 275 | void KateSpellCheckDialog::spellCheckDone() |
| 276 | { |
| 277 | m_currentSpellCheckRange = KTextEditor::Range::invalid(); |
| 278 | m_currentDecToEncOffsetList.clear(); |
| 279 | m_view->clearSelection(); |
| 280 | } |
| 281 | |
| 282 | void KateSpellCheckDialog::objectDestroyed(QObject *object) |
| 283 | { |
| 284 | Q_UNUSED(object); |
| 285 | m_sonnetDialog = nullptr; |
| 286 | } |
| 287 | |
| 288 | void KateSpellCheckDialog::languageChanged(const QString &language) |
| 289 | { |
| 290 | m_userSpellCheckLanguage = language; |
| 291 | } |
| 292 | |
| 293 | // END |
| 294 | |
| 295 | #include "moc_spellcheckdialog.cpp" |
| 296 | |