QtSpell 0.8.5
Spell checking for Qt text widgets
TextEditChecker.cpp
1/* QtSpell - Spell checking for Qt text widgets.
2 * Copyright (c) 2014 Sandro Mani
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
19#include "QtSpell.hpp"
20#include "TextEditChecker_p.hpp"
21#include "UndoRedoStack.hpp"
22
23#include <QDebug>
24#include <QPlainTextEdit>
25#include <QTextEdit>
26#include <QTextBlock>
27
28namespace QtSpell {
29
30QString TextCursor::nextChar(int num) const
31{
32 TextCursor testCursor(*this);
33 if(num > 1)
34 testCursor.movePosition(NextCharacter, MoveAnchor, num - 1);
35 else
36 testCursor.setPosition(testCursor.position());
37 testCursor.movePosition(NextCharacter, KeepAnchor);
38 return testCursor.selectedText();
39}
40
41QString TextCursor::prevChar(int num) const
42{
43 TextCursor testCursor(*this);
44 if(num > 1)
45 testCursor.movePosition(PreviousCharacter, MoveAnchor, num - 1);
46 else
47 testCursor.setPosition(testCursor.position());
48 testCursor.movePosition(PreviousCharacter, KeepAnchor);
49 return testCursor.selectedText();
50}
51
52void TextCursor::moveWordStart(MoveMode moveMode)
53{
54 movePosition(StartOfWord, moveMode);
55 qDebug() << "Start: " << position() << ": " << prevChar(2) << prevChar() << "|" << nextChar();
56 // If we are in front of a quote...
57 if(nextChar() == "'"){
58 // If the previous char is alphanumeric, move left one word, otherwise move right one char
59 if(prevChar().contains(m_wordRegEx)){
60 movePosition(WordLeft, moveMode);
61 }else{
62 movePosition(NextCharacter, moveMode);
63 }
64 }
65 // If the previous char is a quote, and the char before that is alphanumeric, move left one word
66 else if(prevChar() == "'" && prevChar(2).contains(m_wordRegEx)){
67 movePosition(WordLeft, moveMode, 2); // 2: because quote counts as a word boundary
68 }
69}
70
71void TextCursor::moveWordEnd(MoveMode moveMode)
72{
73 movePosition(EndOfWord, moveMode);
74 qDebug() << "End: " << position() << ": " << prevChar() << " | " << nextChar() << "|" << nextChar(2);
75 // If we are in behind of a quote...
76 if(prevChar() == "'"){
77 // If the next char is alphanumeric, move right one word, otherwise move left one char
78 if(nextChar().contains(m_wordRegEx)){
79 movePosition(WordRight, moveMode);
80 }else{
81 movePosition(PreviousCharacter, moveMode);
82 }
83 }
84 // If the next char is a quote, and the char after that is alphanumeric, move right one word
85 else if(nextChar() == "'" && nextChar(2).contains(m_wordRegEx)){
86 movePosition(WordRight, moveMode, 2); // 2: because quote counts as a word boundary
87 }
88}
89
91
93 : Checker(parent)
94{
95 m_textEdit = 0;
96 m_document = 0;
97 m_undoRedoStack = 0;
98 m_undoRedoInProgress = false;
99 m_noSpellingProperty = -1;
100}
101
103{
104 setTextEdit(reinterpret_cast<TextEditProxy*>(0));
105}
106
107void TextEditChecker::setTextEdit(QTextEdit* textEdit)
108{
109 setTextEdit(textEdit ? new TextEditProxyT<QTextEdit>(textEdit) : reinterpret_cast<TextEditProxyT<QTextEdit>*>(0));
110}
111
112void TextEditChecker::setTextEdit(QPlainTextEdit* textEdit)
113{
114 setTextEdit(textEdit ? new TextEditProxyT<QPlainTextEdit>(textEdit) : reinterpret_cast<TextEditProxyT<QPlainTextEdit>*>(0));
115}
116
117void TextEditChecker::setTextEdit(TextEditProxy *textEdit)
118{
119 if(!textEdit && m_textEdit){
120 disconnect(m_textEdit->object(), SIGNAL(destroyed()), this, SLOT(slotDetachTextEdit()));
121 disconnect(m_textEdit->object(), SIGNAL(textChanged()), this, SLOT(slotCheckDocumentChanged()));
122 disconnect(m_textEdit->object(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowContextMenu(QPoint)));
123 disconnect(m_textEdit->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
124 m_textEdit->setContextMenuPolicy(m_oldContextMenuPolicy);
125 m_textEdit->removeEventFilter(this);
126
127 // Remove spelling format
128 QTextCursor cursor = m_textEdit->textCursor();
129 cursor.movePosition(QTextCursor::Start);
130 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
131 QTextCharFormat fmt = cursor.charFormat();
132 QTextCharFormat defaultFormat = QTextCharFormat();
133 fmt.setFontUnderline(defaultFormat.fontUnderline());
134 fmt.setUnderlineColor(defaultFormat.underlineColor());
135 fmt.setUnderlineStyle(defaultFormat.underlineStyle());
136 cursor.setCharFormat(fmt);
137 }
138 bool undoWasEnabled = m_undoRedoStack != 0;
139 setUndoRedoEnabled(false);
140 delete m_textEdit;
141 m_document = 0;
142 m_textEdit = textEdit;
143 if(m_textEdit){
144 m_document = m_textEdit->document();
145 connect(m_textEdit->object(), SIGNAL(destroyed()), this, SLOT(slotDetachTextEdit()));
146 connect(m_textEdit->object(), SIGNAL(textChanged()), this, SLOT(slotCheckDocumentChanged()));
147 connect(m_textEdit->object(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowContextMenu(QPoint)));
148 connect(m_textEdit->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
149 m_oldContextMenuPolicy = m_textEdit->contextMenuPolicy();
150 setUndoRedoEnabled(undoWasEnabled);
151 m_textEdit->setContextMenuPolicy(Qt::CustomContextMenu);
152 m_textEdit->installEventFilter(this);
154 }
155}
156
157bool TextEditChecker::eventFilter(QObject* obj, QEvent* event)
158{
159 if(event->type() == QEvent::KeyPress){
160 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
161 if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == Qt::CTRL){
162 undo();
163 return true;
164 }else if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == (Qt::CTRL | Qt::SHIFT)){
165 redo();
166 return true;
167 }
168 }
169 return QObject::eventFilter(obj, event);
170}
171
172void TextEditChecker::checkSpelling(int start, int end)
173{
174 if(end == -1){
175 QTextCursor tmpCursor(m_textEdit->textCursor());
176 tmpCursor.movePosition(QTextCursor::End);
177 end = tmpCursor.position();
178 }
179
180 // stop contentsChange signals from being emitted due to changed charFormats
181 m_textEdit->document()->blockSignals(true);
182
183 qDebug() << "Checking range " << start << " - " << end;
184
185 QTextCharFormat errorFmt;
186 errorFmt.setFontUnderline(true);
187 errorFmt.setUnderlineColor(Qt::red);
188 errorFmt.setUnderlineStyle(QTextCharFormat::WaveUnderline);
189 QTextCharFormat defaultFormat = QTextCharFormat();
190
191 TextCursor cursor(m_textEdit->textCursor());
192 cursor.beginEditBlock();
193 cursor.setPosition(start);
194 while(cursor.position() < end) {
195 cursor.moveWordEnd(QTextCursor::KeepAnchor);
196 bool correct;
197 QString word = cursor.selectedText();
198 if(noSpellingPropertySet(cursor)) {
199 correct = true;
200 qDebug() << "Skipping word:" << word << "(" << cursor.anchor() << "-" << cursor.position() << ")";
201 } else {
202 correct = checkWord(word);
203 qDebug() << "Checking word:" << word << "(" << cursor.anchor() << "-" << cursor.position() << "), correct:" << correct;
204 }
205 if(!correct){
206 cursor.mergeCharFormat(errorFmt);
207 }else{
208 QTextCharFormat fmt = cursor.charFormat();
209 fmt.setFontUnderline(defaultFormat.fontUnderline());
210 fmt.setUnderlineColor(defaultFormat.underlineColor());
211 fmt.setUnderlineStyle(defaultFormat.underlineStyle());
212 cursor.setCharFormat(fmt);
213 }
214 // Go to next word start
215 while(cursor.position() < end && !cursor.isWordChar(cursor.nextChar())){
216 cursor.movePosition(QTextCursor::NextCharacter);
217 }
218 }
219 cursor.endEditBlock();
220
221 m_textEdit->document()->blockSignals(false);
222}
223
224bool TextEditChecker::noSpellingPropertySet(const QTextCursor &cursor) const
225{
226 if(m_noSpellingProperty < QTextFormat::UserProperty) {
227 return false;
228 }
229 if(cursor.charFormat().intProperty(m_noSpellingProperty) == 1) {
230 return true;
231 }
232 const QList<QTextLayout::FormatRange>& formats = cursor.block().layout()->additionalFormats();
233 int pos = cursor.positionInBlock();
234 foreach(const QTextLayout::FormatRange& range, formats) {
235 if(pos > range.start && pos <= range.start + range.length && range.format.intProperty(m_noSpellingProperty) == 1) {
236 return true;
237 }
238 }
239 return false;
240}
241
243{
244 if(m_undoRedoStack){
245 m_undoRedoStack->clear();
246 }
247}
248
250{
251 if(enabled == (m_undoRedoStack != 0)){
252 return;
253 }
254 if(!enabled){
255 delete m_undoRedoStack;
256 m_undoRedoStack = 0;
257 emit undoAvailable(false);
258 emit redoAvailable(false);
259 }else{
260 m_undoRedoStack = new UndoRedoStack(m_textEdit);
261 connect(m_undoRedoStack, SIGNAL(undoAvailable(bool)), this, SIGNAL(undoAvailable(bool)));
262 connect(m_undoRedoStack, SIGNAL(redoAvailable(bool)), this, SIGNAL(redoAvailable(bool)));
263 }
264}
265
266QString TextEditChecker::getWord(int pos, int* start, int* end) const
267{
268 TextCursor cursor(m_textEdit->textCursor());
269 cursor.setPosition(pos);
270 cursor.moveWordStart();
271 cursor.moveWordEnd(QTextCursor::KeepAnchor);
272 if(start)
273 *start = cursor.anchor();
274 if(end)
275 *end = cursor.position();
276 return cursor.selectedText();
277}
278
279void TextEditChecker::insertWord(int start, int end, const QString &word)
280{
281 QTextCursor cursor(m_textEdit->textCursor());
282 cursor.setPosition(start);
283 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, end - start);
284 cursor.insertText(word);
285}
286
287void TextEditChecker::slotShowContextMenu(const QPoint &pos)
288{
289 QPoint globalPos = m_textEdit->mapToGlobal(pos);
290 QMenu* menu = m_textEdit->createStandardContextMenu();
291 int wordPos = m_textEdit->cursorForPosition(pos).position();
292 showContextMenu(menu, globalPos, wordPos);
293}
294
295void TextEditChecker::slotCheckDocumentChanged()
296{
297 if(m_document != m_textEdit->document()) {
298 bool undoWasEnabled = m_undoRedoStack != 0;
299 setUndoRedoEnabled(false);
300 if(m_document){
301 disconnect(m_document, SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
302 }
303 m_document = m_textEdit->document();
304 connect(m_document, SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
305 setUndoRedoEnabled(undoWasEnabled);
306 }
307}
308
309void TextEditChecker::slotDetachTextEdit()
310{
311 bool undoWasEnabled = m_undoRedoStack != 0;
312 setUndoRedoEnabled(false);
313 // Signals are disconnected when objects are deleted
314 delete m_textEdit;
315 m_textEdit = 0;
316 m_document = 0;
317 if(undoWasEnabled){
318 // Crate dummy instance
319 setUndoRedoEnabled(true);
320 }
321}
322
323void TextEditChecker::slotCheckRange(int pos, int removed, int added)
324{
325 if(m_undoRedoStack != 0 && !m_undoRedoInProgress){
326 m_undoRedoStack->handleContentsChange(pos, removed, added);
327 }
328
329 // Qt Bug? Apparently, when contents is pasted at pos = 0, added and removed are too large by 1
330 TextCursor c(m_textEdit->textCursor());
331 c.movePosition(QTextCursor::End);
332 int len = c.position();
333 if(pos == 0 && added > len){
334 --added;
335 }
336
337 // Set default format on inserted text
338 c.beginEditBlock();
339 c.setPosition(pos);
340 c.moveWordStart();
341 c.setPosition(pos + added, QTextCursor::KeepAnchor);
342 c.moveWordEnd(QTextCursor::KeepAnchor);
343 QTextCharFormat fmt = c.charFormat();
344 QTextCharFormat defaultFormat = QTextCharFormat();
345 fmt.setFontUnderline(defaultFormat.fontUnderline());
346 fmt.setUnderlineColor(defaultFormat.underlineColor());
347 fmt.setUnderlineStyle(defaultFormat.underlineStyle());
348 c.setCharFormat(fmt);
349 checkSpelling(c.anchor(), c.position());
350 c.endEditBlock();
351}
352
354{
355 if(m_undoRedoStack != 0){
356 m_undoRedoInProgress = true;
357 m_undoRedoStack->undo();
358 m_textEdit->ensureCursorVisible();
359 m_undoRedoInProgress = false;
360 }
361}
362
364{
365 if(m_undoRedoStack != 0){
366 m_undoRedoInProgress = true;
367 m_undoRedoStack->redo();
368 m_textEdit->ensureCursorVisible();
369 m_undoRedoInProgress = false;
370 }
371}
372
373} // QtSpell
An abstract class providing spell checking support.
Definition: QtSpell.hpp:58
bool checkWord(const QString &word) const
Check the specified word.
Definition: Checker.cpp:135
An enhanced QTextCursor.
void moveWordStart(MoveMode moveMode=MoveAnchor)
Move the cursor to the start of the current word. Cursor must be inside a word. This method correctly...
QString prevChar(int num=1) const
Retreive the num-th previous character.
bool isWordChar(const QString &character) const
Returns whether the specified character is a word character.
void moveWordEnd(MoveMode moveMode=MoveAnchor)
Move the cursor to the end of the current word. Cursor must be inside a word. This method correctly h...
QString nextChar(int num=1) const
Retreive the num-th next character.
TextEditChecker(QObject *parent=0)
TextEditChecker object constructor.
void setUndoRedoEnabled(bool enabled)
Sets whether undo/redo functionality is enabled.
void clearUndoRedo()
Clears the undo/redo stack.
void setTextEdit(QTextEdit *textEdit)
Set the QTextEdit to check.
void checkSpelling(int start=0, int end=-1)
Check the spelling.
~TextEditChecker()
TextEditChecker object destructor.
void redo()
Redo the last edit operation.
void redoAvailable(bool available)
Emitted when the redo stak changes.
void undo()
Undo the last edit operation.
void undoAvailable(bool available)
Emitted when the undo stack changes.
void insertWord(int start, int end, const QString &word)
Replaces the specified range with the specified word.
QString getWord(int pos, int *start=0, int *end=0) const
Get the word at the specified cursor position.
QtSpell namespace.
Definition: Checker.cpp:65