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