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 
28 namespace QtSpell {
29 
30 TextEditCheckerPrivate::TextEditCheckerPrivate()
31  : CheckerPrivate()
32 {
33 }
34 
35 TextEditCheckerPrivate::~TextEditCheckerPrivate()
36 {
37 }
38 
40 
41 QString 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 
52 QString 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 
63 void 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 
82 void 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 
103 TextEditChecker::TextEditChecker(QObject* parent)
104  : Checker(*new TextEditCheckerPrivate(), parent)
105 {
106 }
107 
109 {
110  Q_D(TextEditChecker);
111  d->setTextEdit(nullptr);
112 }
113 
114 void TextEditChecker::setTextEdit(QTextEdit* textEdit)
115 {
116  Q_D(TextEditChecker);
117  d->setTextEdit(textEdit ? new TextEditProxyT<QTextEdit>(textEdit) : nullptr);
118 }
119 
120 void TextEditChecker::setTextEdit(QPlainTextEdit* textEdit)
121 {
122  Q_D(TextEditChecker);
123  d->setTextEdit(textEdit ? new TextEditProxyT<QPlainTextEdit>(textEdit) : nullptr);
124 }
125 
126 void 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 
186 bool 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 
201 void 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 
257 bool 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 
301 QString 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 
315 void 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 
324 void 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 
333 void 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 
348 void 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 
362 void 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