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 <KStandardAction> |
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: KStandardAction::Spelling, receiver: this, SLOT(spellcheck())); |
50 | |
51 | QAction *a = new QAction(i18n("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 | |