1 /* This file is part of the KDE project
2 * Copyright (C) 2007, 2008 Fredy Yanardi <fyanardi@gmail.com>
3 * Copyright (C) 2007,2009,2010 Thomas Zander <zander@kde.org>
4 * Copyright (C) 2010 Christoph Goerlich <chgoerlich@gmx.de>
5 * Copyright (C) 2012 Shreya Pandit <shreya@shreyapandit.com>
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Library General Public
9 * License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Library General Public License for more details.
16 *
17 * You should have received a copy of the GNU Library General Public License
18 * along with this library; see the file COPYING.LIB. If not, write to
19 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20 * Boston, MA 02110-1301, USA.
21 */
22
23 #include "SpellCheck.h"
24 #include "BgSpellCheck.h"
25 #include "SpellCheckMenu.h"
26 #include "SpellCheckDebug.h"
27
28 #include <KoCharacterStyle.h>
29 #include <KoTextBlockData.h>
30 #include <KoTextDocumentLayout.h>
31 #include <KoTextLayoutRootAreaProvider.h>
32
33 #include <KSharedConfig>
34 #include <klocalizedstring.h>
35 #include <kconfiggroup.h>
36 #include <ktoggleaction.h>
37 #include <sonnet/configdialog.h>
38
39 #include <QTextBlock>
40 #include <QThread>
41 #include <QTimer>
42 #include <QApplication>
43 #include <QTextCharFormat>
44 #include <QAction>
45
SpellCheck()46 SpellCheck::SpellCheck()
47 : m_document(0)
48 , m_bgSpellCheck(0)
49 , m_enableSpellCheck(true)
50 , m_documentIsLoading(false)
51 , m_isChecking(false)
52 , m_spellCheckMenu(0)
53 , m_activeSection(0, 0, 0)
54 , m_simpleEdit(false)
55 , m_cursorPosition(0)
56 {
57 /* setup actions for this plugin */
58 QAction *configureAction = new QAction(i18n("Configure &Spell Checking..."), this);
59 connect(configureAction, SIGNAL(triggered()), this, SLOT(configureSpellCheck()));
60 addAction("tool_configure_spellcheck", configureAction);
61
62 KToggleAction *spellCheck = new KToggleAction(i18n("Auto Spell Check"), this);
63 addAction("tool_auto_spellcheck", spellCheck);
64
65 KConfigGroup spellConfig = KSharedConfig::openConfig()->group("Spelling");
66 m_enableSpellCheck = spellConfig.readEntry("autoSpellCheck", m_enableSpellCheck);
67 spellCheck->setChecked(m_enableSpellCheck);
68 m_speller = Sonnet::Speller(spellConfig.readEntry("defaultLanguage", "en_US"));
69 m_bgSpellCheck = new BgSpellCheck(m_speller, this);
70
71 m_spellCheckMenu = new SpellCheckMenu(m_speller, this);
72 QPair<QString, QAction*> pair = m_spellCheckMenu->menuAction();
73 addAction(pair.first, pair.second);
74
75 connect(m_bgSpellCheck, SIGNAL(misspelledWord(QString,int,bool)),
76 this, SLOT(highlightMisspelled(QString,int,bool)));
77 connect(m_bgSpellCheck, SIGNAL(done()), this, SLOT(finishedRun()));
78 connect(spellCheck, SIGNAL(toggled(bool)), this, SLOT(setBackgroundSpellChecking(bool)));
79 }
80
finishedWord(QTextDocument * document,int cursorPosition)81 void SpellCheck::finishedWord(QTextDocument *document, int cursorPosition)
82 {
83 setDocument(document);
84 if (!m_enableSpellCheck)
85 return;
86
87 QTextBlock block = document->findBlock(cursorPosition);
88 if (!block.isValid())
89 return;
90 KoTextBlockData blockData(block);
91 blockData.setMarkupsLayoutValidity(KoTextBlockData::Misspell, false);
92 checkSection(document, block.position(), block.position() + block.length() - 1);
93 }
94
finishedParagraph(QTextDocument * document,int cursorPosition)95 void SpellCheck::finishedParagraph(QTextDocument *document, int cursorPosition)
96 {
97 setDocument(document);
98 Q_UNUSED(document);
99 Q_UNUSED(cursorPosition);
100 }
101
startingSimpleEdit(QTextDocument * document,int cursorPosition)102 void SpellCheck::startingSimpleEdit(QTextDocument *document, int cursorPosition)
103 {
104 m_simpleEdit = true;
105 setDocument(document);
106 m_cursorPosition = cursorPosition;
107 }
108
checkSection(QTextDocument * document,int startPosition,int endPosition)109 void SpellCheck::checkSection(QTextDocument *document, int startPosition, int endPosition)
110 {
111 if (startPosition >= endPosition) { // no work
112 return;
113 }
114
115 foreach (const SpellSections &ss, m_documentsQueue) {
116 if (ss.from <= startPosition && ss.to >= endPosition) {
117 runQueue();
118 m_spellCheckMenu->setVisible(true);
119 return;
120 }
121 // TODO also check if we should replace an existing queued item with a longer span
122 }
123
124 SpellSections ss(document, startPosition, endPosition);
125 m_documentsQueue.enqueue(ss);
126 runQueue();
127 m_spellCheckMenu->setVisible(true);
128 }
129
setDocument(QTextDocument * document)130 void SpellCheck::setDocument(QTextDocument *document)
131 {
132 if (m_document == document)
133 return;
134 if (m_document)
135 disconnect (document, SIGNAL(contentsChange(int,int,int)), this, SLOT(documentChanged(int,int,int)));
136
137 m_document = document;
138 connect (document, SIGNAL(contentsChange(int,int,int)), this, SLOT(documentChanged(int,int,int)));
139 }
140
availableBackends() const141 QStringList SpellCheck::availableBackends() const
142 {
143 return m_speller.availableBackends();
144 }
145
availableLanguages() const146 QStringList SpellCheck::availableLanguages() const
147 {
148 return m_speller.availableLanguages();
149 }
150
setDefaultLanguage(const QString & language)151 void SpellCheck::setDefaultLanguage(const QString &language)
152 {
153 m_speller.setDefaultLanguage(language);
154 m_bgSpellCheck->setDefaultLanguage(language);
155 if (m_enableSpellCheck && m_document) {
156 checkSection(m_document, 0, m_document->characterCount() - 1);
157 }
158 }
159
setBackgroundSpellChecking(bool on)160 void SpellCheck::setBackgroundSpellChecking(bool on)
161 {
162 if (m_enableSpellCheck == on)
163 return;
164 KConfigGroup spellConfig = KSharedConfig::openConfig()->group("Spelling");
165 m_enableSpellCheck = on;
166 spellConfig.writeEntry("autoSpellCheck", m_enableSpellCheck);
167 if (m_document) {
168 if (!m_enableSpellCheck) {
169 for (QTextBlock block = m_document->begin(); block != m_document->end(); block = block.next()) {
170 KoTextBlockData blockData(block);
171 blockData.clearMarkups(KoTextBlockData::Misspell);
172 }
173 m_spellCheckMenu->setEnabled(false);
174 m_spellCheckMenu->setVisible(false);
175 } else {
176 //when re-enabling 'Auto Spell Check' we want spellchecking the whole document
177 checkSection(m_document, 0, m_document->characterCount() - 1);
178 m_spellCheckMenu->setVisible(true);
179 }
180 }
181 }
182
183
setSkipAllUppercaseWords(bool on)184 void SpellCheck::setSkipAllUppercaseWords(bool on)
185 {
186 m_speller.setAttribute(Speller::CheckUppercase, !on);
187 }
188
setSkipRunTogetherWords(bool on)189 void SpellCheck::setSkipRunTogetherWords(bool on)
190 {
191 m_speller.setAttribute(Speller::SkipRunTogether, on);
192 }
193
addWordToPersonal(const QString & word,int startPosition)194 bool SpellCheck::addWordToPersonal(const QString &word, int startPosition)
195 {
196 QTextBlock block = m_document->findBlock(startPosition);
197 if (!block.isValid())
198 return false;
199
200 KoTextBlockData blockData(block);
201 blockData.setMarkupsLayoutValidity(KoTextBlockData::Misspell, false);
202 checkSection(m_document, block.position(), block.position() + block.length() - 1);
203 // TODO we should probably recheck the entire document so other occurrences are also removed, but then again we should recheck every document (footer,header etc) not sure how to do this
204 return m_bgSpellCheck->addWordToPersonal(word);
205 }
206
207
defaultLanguage() const208 QString SpellCheck::defaultLanguage() const
209 {
210 return m_speller.defaultLanguage();
211 }
212
backgroundSpellChecking()213 bool SpellCheck::backgroundSpellChecking()
214 {
215 return m_enableSpellCheck;
216 }
217
skipAllUppercaseWords()218 bool SpellCheck::skipAllUppercaseWords()
219 {
220 return m_speller.testAttribute(Speller::CheckUppercase);
221 }
222
skipRunTogetherWords()223 bool SpellCheck::skipRunTogetherWords()
224 {
225 return m_speller.testAttribute(Speller::SkipRunTogether);
226 }
227
228 // TODO:
229 // 1) When editing a misspelled word it should be spellchecked on the fly so the markup is removed when it is OK.
230 // 2) Deleting a character should be treated as a simple edit
highlightMisspelled(const QString & word,int startPosition,bool misspelled)231 void SpellCheck::highlightMisspelled(const QString &word, int startPosition, bool misspelled)
232 {
233 if (!misspelled)
234 return;
235
236 #if 0
237 // DEBUG
238 class MyThread : public QThread { public: static void mySleep(unsigned long msecs) { msleep(msecs); }};
239 static_cast<MyThread*>(QThread::currentThread())->mySleep(400);
240 #endif
241
242 QTextBlock block = m_activeSection.document->findBlock(startPosition);
243 KoTextBlockData blockData(block);
244 blockData.appendMarkup(KoTextBlockData::Misspell, startPosition - block.position(), startPosition - block.position() + word.trimmed().length());
245 }
246
documentChanged(int from,int charsRemoved,int charsAdded)247 void SpellCheck::documentChanged(int from, int charsRemoved, int charsAdded)
248 {
249 QTextDocument *document = qobject_cast<QTextDocument*>(sender());
250 if (document == 0)
251 return;
252
253 // If a simple edit, we use the cursor position to determine where
254 // the change occurred. This makes it possible to handle cases
255 // where formatting of a block has changed, eg. when dropcaps is used.
256 // QTextDocument then reports the change as if the whole block has changed.
257 // Ex: Having a 10 char line and you add a char at pos 7:
258 // from = block->position()
259 // charsRemoved = 10
260 // charsAdded = 11
261 // m_cursorPosition = 7
262 int pos = m_simpleEdit ? m_cursorPosition : from;
263 QTextBlock block = document->findBlock(pos);
264 if (!block.isValid())
265 return;
266
267 do {
268 KoTextBlockData blockData(block);
269 if (m_enableSpellCheck) {
270 // This block and all blocks after this must be relayouted
271 blockData.setMarkupsLayoutValidity(KoTextBlockData::Misspell, false);
272 // If it's a simple edit we will wait until finishedWord before spellchecking
273 // but we need to adjust all markups behind the added/removed character(s)
274 if (m_simpleEdit) {
275 // Since markups work on positions within each block only the edited block must be rebased
276 if (block.position() <= pos) {
277 blockData.rebaseMarkups(KoTextBlockData::Misspell, pos - block.position(), charsAdded - charsRemoved);
278 }
279 } else {
280 // handle not so simple edits (like cut/paste etc)
281 checkSection(document, block.position(), block.position() + block.length() - 1);
282 }
283 } else {
284 blockData.clearMarkups(KoTextBlockData::Misspell);
285 }
286 block = block.next();
287 } while(block.isValid() && block.position() <= from + charsAdded);
288
289 m_simpleEdit = false;
290 }
291
runQueue()292 void SpellCheck::runQueue()
293 {
294 Q_ASSERT(QThread::currentThread() == QApplication::instance()->thread());
295 if (m_isChecking)
296 return;
297 while (!m_documentsQueue.isEmpty()) {
298 m_activeSection = m_documentsQueue.dequeue();
299 if (m_activeSection.document.isNull())
300 continue;
301 QTextBlock block = m_activeSection.document->findBlock(m_activeSection.from);
302 if (!block.isValid())
303 continue;
304 m_isChecking = true;
305 do {
306 KoTextBlockData blockData(block);
307 blockData.clearMarkups(KoTextBlockData::Misspell);
308 block = block.next();
309 } while(block.isValid() && block.position() < m_activeSection.to);
310
311 m_bgSpellCheck->startRun(m_activeSection.document, m_activeSection.from, m_activeSection.to);
312 break;
313 }
314 }
315
configureSpellCheck()316 void SpellCheck::configureSpellCheck()
317 {
318 Sonnet::ConfigDialog *dialog = new Sonnet::ConfigDialog(0);
319 connect (dialog, SIGNAL(languageChanged(QString)), this, SLOT(setDefaultLanguage(QString)));
320 dialog->exec();
321 delete dialog;
322 }
323
finishedRun()324 void SpellCheck::finishedRun()
325 {
326 Q_ASSERT(QThread::currentThread() == QApplication::instance()->thread());
327 m_isChecking = false;
328
329 KoTextDocumentLayout *lay = qobject_cast<KoTextDocumentLayout*>(m_activeSection.document->documentLayout());
330 lay->provider()->updateAll();
331
332 QTimer::singleShot(0, this, SLOT(runQueue()));
333 }
334
setCurrentCursorPosition(QTextDocument * document,int cursorPosition)335 void SpellCheck::setCurrentCursorPosition(QTextDocument *document, int cursorPosition)
336 {
337 setDocument(document);
338 if (m_enableSpellCheck) {
339 //check if word at cursor is misspelled
340 QTextBlock block = m_document->findBlock(cursorPosition);
341 if (block.isValid()) {
342 KoTextBlockData blockData(block);
343 KoTextBlockData::MarkupRange range = blockData.findMarkup(KoTextBlockData::Misspell, cursorPosition - block.position());
344 if (int length = range.lastChar - range.firstChar) {
345 QString word = block.text().mid(range.firstChar, length);
346 m_spellCheckMenu->setMisspelled(word, block.position() + range.firstChar, length);
347 QString language = m_bgSpellCheck->currentLanguage();
348 if (!m_bgSpellCheck->currentLanguage().isEmpty() && !m_bgSpellCheck->currentCountry().isEmpty()) {
349 language += '_';
350 }
351 language += m_bgSpellCheck->currentCountry();
352 m_spellCheckMenu->setCurrentLanguage(language);
353 m_spellCheckMenu->setVisible(true);
354 m_spellCheckMenu->setEnabled(true);
355 return;
356 }
357 m_spellCheckMenu->setEnabled(false);
358 } else {
359 m_spellCheckMenu->setEnabled(false);
360 }
361 }
362 }
363
replaceWordBySuggestion(const QString & word,int startPosition,int lengthOfWord)364 void SpellCheck::replaceWordBySuggestion(const QString &word, int startPosition, int lengthOfWord)
365 {
366 if (!m_document)
367 return;
368
369 QTextBlock block = m_document->findBlock(startPosition);
370 if (!block.isValid())
371 return;
372
373 QTextCursor cursor(m_document);
374 cursor.setPosition(startPosition);
375 cursor.movePosition(QTextCursor::NextCharacter,QTextCursor::KeepAnchor, lengthOfWord);
376 cursor.removeSelectedText();
377 cursor.insertText(word);
378 }
379