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 
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  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 
107 void TextEditChecker::setTextEdit(QTextEdit* textEdit)
108 {
109  setTextEdit(textEdit ? new TextEditProxyT<QTextEdit>(textEdit) : reinterpret_cast<TextEditProxyT<QTextEdit>*>(0));
110 }
111 
112 void TextEditChecker::setTextEdit(QPlainTextEdit* textEdit)
113 {
114  setTextEdit(textEdit ? new TextEditProxyT<QPlainTextEdit>(textEdit) : reinterpret_cast<TextEditProxyT<QPlainTextEdit>*>(0));
115 }
116 
117 void 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);
153  checkSpelling();
154  }
155 }
156 
157 bool 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 
172 void 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 
224 bool 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 
266 QString 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 
279 void 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 
287 void 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 
295 void 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 
309 void 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 
323 void 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