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
QtSpell::TextCursor::moveWordStart
void moveWordStart(MoveMode moveMode=MoveAnchor)
Move the cursor to the start of the current word. Cursor must be inside a word. This method correctly...
Definition: TextEditChecker.cpp:52
QtSpell::TextEditChecker::redoAvailable
void redoAvailable(bool available)
Emitted when the redo stak changes.
QtSpell::TextEditChecker::getWord
QString getWord(int pos, int *start=0, int *end=0) const
Get the word at the specified cursor position.
Definition: TextEditChecker.cpp:261
QtSpell
QtSpell namespace.
Definition: Checker.cpp:66
QtSpell::TextCursor::moveWordEnd
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...
Definition: TextEditChecker.cpp:71
QtSpell::TextCursor::isWordChar
bool isWordChar(const QString &character) const
Returns whether the specified character is a word character.
Definition: TextEditChecker_p.hpp:83
QtSpell::TextEditChecker::clearUndoRedo
void clearUndoRedo()
Clears the undo/redo stack.
Definition: TextEditChecker.cpp:237
QtSpell::TextCursor::prevChar
QString prevChar(int num=1) const
Retreive the num-th previous character.
Definition: TextEditChecker.cpp:41
QtSpell::TextEditChecker::insertWord
void insertWord(int start, int end, const QString &word)
Replaces the specified range with the specified word.
Definition: TextEditChecker.cpp:274
QtSpell::Checker::checkWord
bool checkWord(const QString &word) const
Check the specified word.
Definition: Checker.cpp:132
QtSpell::TextEditChecker::undo
void undo()
Undo the last edit operation.
Definition: TextEditChecker.cpp:347
QtSpell::TextEditChecker::TextEditChecker
TextEditChecker(QObject *parent=0)
TextEditChecker object constructor.
Definition: TextEditChecker.cpp:92
QtSpell::TextCursor::nextChar
QString nextChar(int num=1) const
Retreive the num-th next character.
Definition: TextEditChecker.cpp:30
QtSpell::Checker
An abstract class providing spell checking support.
Definition: QtSpell.hpp:57
QtSpell::TextEditChecker::redo
void redo()
Redo the last edit operation.
Definition: TextEditChecker.cpp:357
QtSpell::TextEditChecker::setUndoRedoEnabled
void setUndoRedoEnabled(bool enabled)
Sets whether undo/redo functionality is enabled.
Definition: TextEditChecker.cpp:244
QtSpell::TextCursor
An enhanced QTextCursor.
Definition: TextEditChecker_p.hpp:31
QtSpell::TextEditChecker::checkSpelling
void checkSpelling(int start=0, int end=-1)
Check the spelling.
Definition: TextEditChecker.cpp:167
QtSpell::TextEditChecker::~TextEditChecker
~TextEditChecker()
TextEditChecker object destructor.
Definition: TextEditChecker.cpp:97
QtSpell::TextEditChecker::undoAvailable
void undoAvailable(bool available)
Emitted when the undo stack changes.
QtSpell::TextEditChecker::setTextEdit
void setTextEdit(QTextEdit *textEdit)
Set the QTextEdit to check.
Definition: TextEditChecker.cpp:102