QtSpell  0.9.0
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 
28 namespace QtSpell {
29 
30 QString 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 
41 QString 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 
52 void 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 
71 void 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 }
96 
98 {
99  setTextEdit(static_cast<TextEditProxy*>(nullptr));
100 }
101 
102 void TextEditChecker::setTextEdit(QTextEdit* textEdit)
103 {
104  setTextEdit(textEdit ? new TextEditProxyT<QTextEdit>(textEdit) : static_cast<TextEditProxyT<QTextEdit>*>(nullptr));
105 }
106 
107 void TextEditChecker::setTextEdit(QPlainTextEdit* textEdit)
108 {
109  setTextEdit(textEdit ? new TextEditProxyT<QPlainTextEdit>(textEdit) : static_cast<TextEditProxyT<QPlainTextEdit>*>(nullptr));
110 }
111 
112 void TextEditChecker::setTextEdit(TextEditProxy *textEdit)
113 {
114  if(m_textEdit){
115  disconnect(m_textEdit, &TextEditProxy::editDestroyed, this, &TextEditChecker::slotDetachTextEdit);
116  disconnect(m_textEdit, &TextEditProxy::textChanged, this, &TextEditChecker::slotCheckDocumentChanged);
117  disconnect(m_textEdit, &TextEditProxy::customContextMenuRequested, this, &TextEditChecker::slotShowContextMenu);
118  disconnect(m_textEdit->document(), &QTextDocument::contentsChange, this, &TextEditChecker::slotCheckRange);
119  m_textEdit->setContextMenuPolicy(m_oldContextMenuPolicy);
120  m_textEdit->removeEventFilter(this);
121 
122  // Remove spelling format
123  QTextCursor cursor = m_textEdit->textCursor();
124  cursor.movePosition(QTextCursor::Start);
125  cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
126  QTextCharFormat fmt = cursor.charFormat();
127  QTextCharFormat defaultFormat = QTextCharFormat();
128  fmt.setFontUnderline(defaultFormat.fontUnderline());
129  fmt.setUnderlineColor(defaultFormat.underlineColor());
130  fmt.setUnderlineStyle(defaultFormat.underlineStyle());
131  cursor.setCharFormat(fmt);
132  }
133  bool undoWasEnabled = m_undoRedoStack != nullptr;
134  setUndoRedoEnabled(false);
135  delete m_textEdit;
136  m_document = nullptr;
137  m_textEdit = textEdit;
138  if(m_textEdit){
139  m_document = m_textEdit->document();
140  connect(m_textEdit, &TextEditProxy::editDestroyed, this, &TextEditChecker::slotDetachTextEdit);
141  connect(m_textEdit, &TextEditProxy::textChanged, this, &TextEditChecker::slotCheckDocumentChanged);
142  connect(m_textEdit, &TextEditProxy::customContextMenuRequested, this, &TextEditChecker::slotShowContextMenu);
143  connect(m_textEdit->document(), &QTextDocument::contentsChange, this, &TextEditChecker::slotCheckRange);
144  m_oldContextMenuPolicy = m_textEdit->contextMenuPolicy();
145  setUndoRedoEnabled(undoWasEnabled);
146  m_textEdit->setContextMenuPolicy(Qt::CustomContextMenu);
147  m_textEdit->installEventFilter(this);
148  checkSpelling();
149  }
150 }
151 
152 bool TextEditChecker::eventFilter(QObject* obj, QEvent* event)
153 {
154  if(event->type() == QEvent::KeyPress){
155  QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
156  if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == Qt::CTRL){
157  undo();
158  return true;
159  }else if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == (Qt::CTRL | Qt::SHIFT)){
160  redo();
161  return true;
162  }
163  }
164  return QObject::eventFilter(obj, event);
165 }
166 
167 void TextEditChecker::checkSpelling(int start, int end)
168 {
169  if(end == -1){
170  QTextCursor tmpCursor(m_textEdit->textCursor());
171  tmpCursor.movePosition(QTextCursor::End);
172  end = tmpCursor.position();
173  }
174 
175  // stop contentsChange signals from being emitted due to changed charFormats
176  m_textEdit->document()->blockSignals(true);
177 
178  qDebug() << "Checking range " << start << " - " << end;
179 
180  QTextCharFormat errorFmt;
181  errorFmt.setFontUnderline(true);
182  errorFmt.setUnderlineColor(Qt::red);
183  errorFmt.setUnderlineStyle(QTextCharFormat::WaveUnderline);
184  QTextCharFormat defaultFormat = QTextCharFormat();
185 
186  TextCursor cursor(m_textEdit->textCursor());
187  cursor.beginEditBlock();
188  cursor.setPosition(start);
189  while(cursor.position() < end) {
190  cursor.moveWordEnd(QTextCursor::KeepAnchor);
191  bool correct;
192  QString word = cursor.selectedText();
193  if(noSpellingPropertySet(cursor)) {
194  correct = true;
195  qDebug() << "Skipping word:" << word << "(" << cursor.anchor() << "-" << cursor.position() << ")";
196  } else {
197  correct = checkWord(word);
198  qDebug() << "Checking word:" << word << "(" << cursor.anchor() << "-" << cursor.position() << "), correct:" << correct;
199  }
200  if(!correct){
201  cursor.mergeCharFormat(errorFmt);
202  }else{
203  QTextCharFormat fmt = cursor.charFormat();
204  fmt.setFontUnderline(defaultFormat.fontUnderline());
205  fmt.setUnderlineColor(defaultFormat.underlineColor());
206  fmt.setUnderlineStyle(defaultFormat.underlineStyle());
207  cursor.setCharFormat(fmt);
208  }
209  // Go to next word start
210  while(cursor.position() < end && !cursor.isWordChar(cursor.nextChar())){
211  cursor.movePosition(QTextCursor::NextCharacter);
212  }
213  }
214  cursor.endEditBlock();
215 
216  m_textEdit->document()->blockSignals(false);
217 }
218 
219 bool TextEditChecker::noSpellingPropertySet(const QTextCursor &cursor) const
220 {
221  if(m_noSpellingProperty < QTextFormat::UserProperty) {
222  return false;
223  }
224  if(cursor.charFormat().intProperty(m_noSpellingProperty) == 1) {
225  return true;
226  }
227  const QVector<QTextLayout::FormatRange>& formats = cursor.block().layout()->formats();
228  int pos = cursor.positionInBlock();
229  foreach(const QTextLayout::FormatRange& range, formats) {
230  if(pos > range.start && pos <= range.start + range.length && range.format.intProperty(m_noSpellingProperty) == 1) {
231  return true;
232  }
233  }
234  return false;
235 }
236 
238 {
239  if(m_undoRedoStack){
240  m_undoRedoStack->clear();
241  }
242 }
243 
245 {
246  if(enabled == (m_undoRedoStack != nullptr)){
247  return;
248  }
249  if(!enabled){
250  delete m_undoRedoStack;
251  m_undoRedoStack = nullptr;
252  emit undoAvailable(false);
253  emit redoAvailable(false);
254  }else{
255  m_undoRedoStack = new UndoRedoStack(m_textEdit);
256  connect(m_undoRedoStack, &QtSpell::UndoRedoStack::undoAvailable, this, &TextEditChecker::undoAvailable);
257  connect(m_undoRedoStack, &QtSpell::UndoRedoStack::redoAvailable, this, &TextEditChecker::redoAvailable);
258  }
259 }
260 
261 QString TextEditChecker::getWord(int pos, int* start, int* end) const
262 {
263  TextCursor cursor(m_textEdit->textCursor());
264  cursor.setPosition(pos);
265  cursor.moveWordStart();
266  cursor.moveWordEnd(QTextCursor::KeepAnchor);
267  if(start)
268  *start = cursor.anchor();
269  if(end)
270  *end = cursor.position();
271  return cursor.selectedText();
272 }
273 
274 void TextEditChecker::insertWord(int start, int end, const QString &word)
275 {
276  QTextCursor cursor(m_textEdit->textCursor());
277  cursor.setPosition(start);
278  cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, end - start);
279  cursor.insertText(word);
280 }
281 
282 void TextEditChecker::slotShowContextMenu(const QPoint &pos)
283 {
284  QPoint globalPos = m_textEdit->mapToGlobal(pos);
285  QMenu* menu = m_textEdit->createStandardContextMenu();
286  int wordPos = m_textEdit->cursorForPosition(pos).position();
287  showContextMenu(menu, globalPos, wordPos);
288 }
289 
290 void TextEditChecker::slotCheckDocumentChanged()
291 {
292  if(m_document != m_textEdit->document()) {
293  bool undoWasEnabled = m_undoRedoStack != nullptr;
294  setUndoRedoEnabled(false);
295  if(m_document){
296  disconnect(m_document, &QTextDocument::contentsChange, this, &TextEditChecker::slotCheckRange);
297  }
298  m_document = m_textEdit->document();
299  connect(m_document, &QTextDocument::contentsChange, this, &TextEditChecker::slotCheckRange);
300  setUndoRedoEnabled(undoWasEnabled);
301  }
302 }
303 
304 void TextEditChecker::slotDetachTextEdit()
305 {
306  bool undoWasEnabled = m_undoRedoStack != nullptr;
307  setUndoRedoEnabled(false);
308  delete m_textEdit;
309  m_textEdit = nullptr;
310  m_document = nullptr;
311  if(undoWasEnabled){
312  // Crate dummy instance
313  setUndoRedoEnabled(true);
314  }
315 }
316 
317 void TextEditChecker::slotCheckRange(int pos, int removed, int added)
318 {
319  if(m_undoRedoStack != nullptr && !m_undoRedoInProgress){
320  m_undoRedoStack->handleContentsChange(pos, removed, added);
321  }
322 
323  // Qt Bug? Apparently, when contents is pasted at pos = 0, added and removed are too large by 1
324  TextCursor c(m_textEdit->textCursor());
325  c.movePosition(QTextCursor::End);
326  int len = c.position();
327  if(pos == 0 && added > len){
328  --added;
329  }
330 
331  // Set default format on inserted text
332  c.beginEditBlock();
333  c.setPosition(pos);
334  c.moveWordStart();
335  c.setPosition(pos + added, QTextCursor::KeepAnchor);
336  c.moveWordEnd(QTextCursor::KeepAnchor);
337  QTextCharFormat fmt = c.charFormat();
338  QTextCharFormat defaultFormat = QTextCharFormat();
339  fmt.setFontUnderline(defaultFormat.fontUnderline());
340  fmt.setUnderlineColor(defaultFormat.underlineColor());
341  fmt.setUnderlineStyle(defaultFormat.underlineStyle());
342  c.setCharFormat(fmt);
343  checkSpelling(c.anchor(), c.position());
344  c.endEditBlock();
345 }
346 
348 {
349  if(m_undoRedoStack != nullptr){
350  m_undoRedoInProgress = true;
351  m_undoRedoStack->undo();
352  m_textEdit->ensureCursorVisible();
353  m_undoRedoInProgress = false;
354  }
355 }
356 
358 {
359  if(m_undoRedoStack != nullptr){
360  m_undoRedoInProgress = true;
361  m_undoRedoStack->redo();
362  m_textEdit->ensureCursorVisible();
363  m_undoRedoInProgress = false;
364  }
365 }
366 
367 } // 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:132
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:66