QtSpell 1.0.1
Spell checking for Qt text widgets
TextEditChecker.cpp
1/* QtSpell - Spell checking for Qt text widgets.
2 * Copyright (c) 2014-2022 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
30TextEditCheckerPrivate::TextEditCheckerPrivate()
31 : CheckerPrivate()
32{
33}
34
35TextEditCheckerPrivate::~TextEditCheckerPrivate()
36{
37}
38
40
41QString TextCursor::nextChar(int num) const
42{
43 TextCursor testCursor(*this);
44 if(num > 1)
45 testCursor.movePosition(NextCharacter, MoveAnchor, num - 1);
46 else
47 testCursor.setPosition(testCursor.position());
48 testCursor.movePosition(NextCharacter, KeepAnchor);
49 return testCursor.selectedText();
50}
51
52QString TextCursor::prevChar(int num) const
53{
54 TextCursor testCursor(*this);
55 if(num > 1)
56 testCursor.movePosition(PreviousCharacter, MoveAnchor, num - 1);
57 else
58 testCursor.setPosition(testCursor.position());
59 testCursor.movePosition(PreviousCharacter, KeepAnchor);
60 return testCursor.selectedText();
61}
62
63void TextCursor::moveWordStart(MoveMode moveMode)
64{
65 movePosition(StartOfWord, moveMode);
66 qDebug() << "Start: " << position() << ": " << prevChar(2) << prevChar() << "|" << nextChar();
67 // If we are in front of a quote...
68 if(nextChar() == "'"){
69 // If the previous char is alphanumeric, move left one word, otherwise move right one char
70 if(prevChar().contains(m_wordRegEx)){
71 movePosition(WordLeft, moveMode);
72 }else{
73 movePosition(NextCharacter, moveMode);
74 }
75 }
76 // If the previous char is a quote, and the char before that is alphanumeric, move left one word
77 else if(prevChar() == "'" && prevChar(2).contains(m_wordRegEx)){
78 movePosition(WordLeft, moveMode, 2); // 2: because quote counts as a word boundary
79 }
80}
81
82void TextCursor::moveWordEnd(MoveMode moveMode)
83{
84 movePosition(EndOfWord, moveMode);
85 qDebug() << "End: " << position() << ": " << prevChar() << " | " << nextChar() << "|" << nextChar(2);
86 // If we are in behind of a quote...
87 if(prevChar() == "'"){
88 // If the next char is alphanumeric, move right one word, otherwise move left one char
89 if(nextChar().contains(m_wordRegEx)){
90 movePosition(WordRight, moveMode);
91 }else{
92 movePosition(PreviousCharacter, moveMode);
93 }
94 }
95 // If the next char is a quote, and the char after that is alphanumeric, move right one word
96 else if(nextChar() == "'" && nextChar(2).contains(m_wordRegEx)){
97 movePosition(WordRight, moveMode, 2); // 2: because quote counts as a word boundary
98 }
99}
100
102
103TextEditChecker::TextEditChecker(QObject* parent)
104 : Checker(*new TextEditCheckerPrivate(), parent)
105{
106}
107
109{
110 Q_D(TextEditChecker);
111 d->setTextEdit(nullptr);
112}
113
114void TextEditChecker::setTextEdit(QTextEdit* textEdit)
115{
116 Q_D(TextEditChecker);
117 d->setTextEdit(textEdit ? new TextEditProxyT<QTextEdit>(textEdit) : nullptr);
118}
119
120void TextEditChecker::setTextEdit(QPlainTextEdit* textEdit)
121{
122 Q_D(TextEditChecker);
123 d->setTextEdit(textEdit ? new TextEditProxyT<QPlainTextEdit>(textEdit) : nullptr);
124}
125
126void TextEditCheckerPrivate::setTextEdit(TextEditProxy *newTextEdit)
127{
128 Q_Q(TextEditChecker);
129 if(textEdit){
130 QObject::disconnect(textEdit, &TextEditProxy::editDestroyed, q, &TextEditChecker::slotDetachTextEdit);
131 QObject::disconnect(textEdit, &TextEditProxy::textChanged, q, &TextEditChecker::slotCheckDocumentChanged);
132 QObject::disconnect(textEdit, &TextEditProxy::customContextMenuRequested, q, &TextEditChecker::slotShowContextMenu);
133 QObject::disconnect(textEdit->document(), &QTextDocument::contentsChange, q, &TextEditChecker::slotCheckRange);
134 textEdit->setContextMenuPolicy(oldContextMenuPolicy);
135 textEdit->removeEventFilter(q);
136
137 // Remove spelling format
138 QTextCursor cursor = textEdit->textCursor();
139 cursor.movePosition(QTextCursor::Start);
140 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
141 QTextCharFormat fmt = cursor.charFormat();
142 QTextCharFormat defaultFormat = QTextCharFormat();
143 fmt.setFontUnderline(defaultFormat.fontUnderline());
144 fmt.setUnderlineColor(defaultFormat.underlineColor());
145 fmt.setUnderlineStyle(defaultFormat.underlineStyle());
146 cursor.setCharFormat(fmt);
147 }
148 bool undoWasEnabled = undoRedoStack != nullptr;
149 q->setUndoRedoEnabled(false);
150 delete textEdit;
151 document = nullptr;
152 textEdit = newTextEdit;
153 if(textEdit){
154 bool wasModified = textEdit->document()->isModified();
155 document = textEdit->document();
156 QObject::connect(textEdit, &TextEditProxy::editDestroyed, q, &TextEditChecker::slotDetachTextEdit);
157 QObject::connect(textEdit, &TextEditProxy::textChanged, q, &TextEditChecker::slotCheckDocumentChanged);
158 QObject::connect(textEdit, &TextEditProxy::customContextMenuRequested, q, &TextEditChecker::slotShowContextMenu);
159 QObject::connect(textEdit->document(), &QTextDocument::contentsChange, q, &TextEditChecker::slotCheckRange);
160 oldContextMenuPolicy = textEdit->contextMenuPolicy();
161 q->setUndoRedoEnabled(undoWasEnabled);
162 textEdit->setContextMenuPolicy(Qt::CustomContextMenu);
163 textEdit->installEventFilter(q);
164 q->checkSpelling();
165 textEdit->document()->setModified(wasModified);
166 } else {
167 if(undoWasEnabled){
168 // Crate dummy instance
169 q->setUndoRedoEnabled(true);
170 }
171 }
172}
173
175{
176 Q_D(TextEditChecker);
177 d->noSpellingProperty = propertyId;
178}
179
181{
182 Q_D(const TextEditChecker);
183 return d->noSpellingProperty;
184}
185
186bool TextEditChecker::eventFilter(QObject* obj, QEvent* event)
187{
188 if(event->type() == QEvent::KeyPress){
189 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
190 if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == Qt::CTRL){
191 undo();
192 return true;
193 }else if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == (Qt::CTRL | Qt::SHIFT)){
194 redo();
195 return true;
196 }
197 }
198 return QObject::eventFilter(obj, event);
199}
200
201void TextEditChecker::checkSpelling(int start, int end)
202{
203 Q_D(TextEditChecker);
204 if (!d->textEdit) {
205 return;
206 }
207 if(end == -1){
208 QTextCursor tmpCursor(d->textEdit->textCursor());
209 tmpCursor.movePosition(QTextCursor::End);
210 end = tmpCursor.position();
211 }
212
213 // stop contentsChange signals from being emitted due to changed charFormats
214 d->textEdit->document()->blockSignals(true);
215
216 qDebug() << "Checking range " << start << " - " << end;
217
218 QTextCharFormat errorFmt;
219 errorFmt.setFontUnderline(true);
220 errorFmt.setUnderlineColor(Qt::red);
221 errorFmt.setUnderlineStyle(QTextCharFormat::WaveUnderline);
222 QTextCharFormat defaultFormat = QTextCharFormat();
223
224 TextCursor cursor(d->textEdit->textCursor());
225 cursor.beginEditBlock();
226 cursor.setPosition(start);
227 while(cursor.position() < end) {
228 cursor.moveWordEnd(QTextCursor::KeepAnchor);
229 bool correct;
230 QString word = cursor.selectedText();
231 if(d->noSpellingPropertySet(cursor)) {
232 correct = true;
233 qDebug() << "Skipping word:" << word << "(" << cursor.anchor() << "-" << cursor.position() << ")";
234 } else {
235 correct = checkWord(word);
236 qDebug() << "Checking word:" << word << "(" << cursor.anchor() << "-" << cursor.position() << "), correct:" << correct;
237 }
238 if(!correct){
239 cursor.mergeCharFormat(errorFmt);
240 }else{
241 QTextCharFormat fmt = cursor.charFormat();
242 fmt.setFontUnderline(defaultFormat.fontUnderline());
243 fmt.setUnderlineColor(defaultFormat.underlineColor());
244 fmt.setUnderlineStyle(defaultFormat.underlineStyle());
245 cursor.setCharFormat(fmt);
246 }
247 // Go to next word start
248 while(cursor.position() < end && !cursor.isWordChar(cursor.nextChar())){
249 cursor.movePosition(QTextCursor::NextCharacter);
250 }
251 }
252 cursor.endEditBlock();
253
254 d->textEdit->document()->blockSignals(false);
255}
256
257bool TextEditCheckerPrivate::noSpellingPropertySet(const QTextCursor &cursor) const
258{
259 if(noSpellingProperty < QTextFormat::UserProperty) {
260 return false;
261 }
262 if(cursor.charFormat().intProperty(noSpellingProperty) == 1) {
263 return true;
264 }
265 const QVector<QTextLayout::FormatRange>& formats = cursor.block().layout()->formats();
266 int pos = cursor.positionInBlock();
267 foreach(const QTextLayout::FormatRange& range, formats) {
268 if(pos > range.start && pos <= range.start + range.length && range.format.intProperty(noSpellingProperty) == 1) {
269 return true;
270 }
271 }
272 return false;
273}
274
276{
277 Q_D(TextEditChecker);
278 if(d->undoRedoStack){
279 d->undoRedoStack->clear();
280 }
281}
282
284{
285 Q_D(TextEditChecker);
286 if(enabled == (d->undoRedoStack != nullptr)){
287 return;
288 }
289 if(!enabled){
290 delete d->undoRedoStack;
291 d->undoRedoStack = nullptr;
292 emit undoAvailable(false);
293 emit redoAvailable(false);
294 }else{
295 d->undoRedoStack = new UndoRedoStack(d->textEdit);
296 connect(d->undoRedoStack, &QtSpell::UndoRedoStack::undoAvailable, this, &TextEditChecker::undoAvailable);
297 connect(d->undoRedoStack, &QtSpell::UndoRedoStack::redoAvailable, this, &TextEditChecker::redoAvailable);
298 }
299}
300
301QString TextEditChecker::getWord(int pos, int* start, int* end) const
302{
303 Q_D(const TextEditChecker);
304 TextCursor cursor(d->textEdit->textCursor());
305 cursor.setPosition(pos);
306 cursor.moveWordStart();
307 cursor.moveWordEnd(QTextCursor::KeepAnchor);
308 if(start)
309 *start = cursor.anchor();
310 if(end)
311 *end = cursor.position();
312 return cursor.selectedText();
313}
314
315void TextEditChecker::insertWord(int start, int end, const QString &word)
316{
317 Q_D(TextEditChecker);
318 QTextCursor cursor(d->textEdit->textCursor());
319 cursor.setPosition(start);
320 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, end - start);
321 cursor.insertText(word);
322}
323
324void TextEditChecker::slotShowContextMenu(const QPoint &pos)
325{
326 Q_D(TextEditChecker);
327 QPoint globalPos = d->textEdit->mapToGlobal(pos);
328 QMenu* menu = d->textEdit->createStandardContextMenu();
329 int wordPos = d->textEdit->cursorForPosition(pos).position();
330 showContextMenu(menu, globalPos, wordPos);
331}
332
333void TextEditChecker::slotCheckDocumentChanged()
334{
335 Q_D(TextEditChecker);
336 if(d->document != d->textEdit->document()) {
337 bool undoWasEnabled = d->undoRedoStack != nullptr;
338 setUndoRedoEnabled(false);
339 if(d->document){
340 disconnect(d->document, &QTextDocument::contentsChange, this, &TextEditChecker::slotCheckRange);
341 }
342 d->document = d->textEdit->document();
343 connect(d->document, &QTextDocument::contentsChange, this, &TextEditChecker::slotCheckRange);
344 setUndoRedoEnabled(undoWasEnabled);
345 }
346}
347
348void TextEditChecker::slotDetachTextEdit()
349{
350 Q_D(TextEditChecker);
351 bool undoWasEnabled = d->undoRedoStack != nullptr;
352 setUndoRedoEnabled(false);
353 delete d->textEdit;
354 d->textEdit = nullptr;
355 d->document = nullptr;
356 if(undoWasEnabled){
357 // Crate dummy instance
358 setUndoRedoEnabled(true);
359 }
360}
361
362void TextEditChecker::slotCheckRange(int pos, int removed, int added)
363{
364 Q_D(TextEditChecker);
365 if(d->undoRedoStack != nullptr && !d->undoRedoInProgress){
366 d->undoRedoStack->handleContentsChange(pos, removed, added);
367 }
368
369 // Qt Bug? Apparently, when contents is pasted at pos = 0, added and removed are too large by 1
370 TextCursor c(d->textEdit->textCursor());
371 c.movePosition(QTextCursor::End);
372 int len = c.position();
373 if(pos == 0 && added > len){
374 --added;
375 }
376
377 // Set default format on inserted text
378 c.beginEditBlock();
379 c.setPosition(pos);
380 c.moveWordStart();
381 c.setPosition(pos + added, QTextCursor::KeepAnchor);
382 c.moveWordEnd(QTextCursor::KeepAnchor);
383 QTextCharFormat fmt = c.charFormat();
384 QTextCharFormat defaultFormat = QTextCharFormat();
385 fmt.setFontUnderline(defaultFormat.fontUnderline());
386 fmt.setUnderlineColor(defaultFormat.underlineColor());
387 fmt.setUnderlineStyle(defaultFormat.underlineStyle());
388 c.setCharFormat(fmt);
389 checkSpelling(c.anchor(), c.position());
390 c.endEditBlock();
391}
392
394{
395 Q_D(TextEditChecker);
396 if(d->undoRedoStack != nullptr){
397 d->undoRedoInProgress = true;
398 d->undoRedoStack->undo();
399 d->textEdit->ensureCursorVisible();
400 d->undoRedoInProgress = false;
401 }
402}
403
405{
406 Q_D(TextEditChecker);
407 if(d->undoRedoStack != nullptr){
408 d->undoRedoInProgress = true;
409 d->undoRedoStack->redo();
410 d->textEdit->ensureCursorVisible();
411 d->undoRedoInProgress = false;
412 }
413}
414
416{
417 Q_D(const TextEditChecker);
418 return d->textEdit != 0;
419}
420
421} // QtSpell
An abstract class providing spell checking support.
Definition: QtSpell.hpp:50
bool checkWord(const QString &word) const
Check the specified word.
Definition: Checker.cpp:204
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...
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.
Checker class for QTextEdit widgets.
Definition: QtSpell.hpp:221
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 setNoSpellingPropertyId(int propertyId)
Set the QTextCharFormat property identifier which marks whether a word ought to be spell-checked.
bool isAttached() const
Returns whether a widget is attached to the checker.
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.
int noSpellingPropertyId() const
Returns the current QTextCharFormat property identifier which marks whether a word ought to be spell-...
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:77