QtSpell  0.7.4
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 
27 namespace QtSpell {
28 
29 QString TextCursor::nextChar(int num) const
30 {
31  TextCursor testCursor(*this);
32  if(num > 1)
33  testCursor.movePosition(NextCharacter, MoveAnchor, num - 1);
34  else
35  testCursor.setPosition(testCursor.position());
36  testCursor.movePosition(NextCharacter, KeepAnchor);
37  return testCursor.selectedText();
38 }
39 
40 QString TextCursor::prevChar(int num) const
41 {
42  TextCursor testCursor(*this);
43  if(num > 1)
44  testCursor.movePosition(PreviousCharacter, MoveAnchor, num - 1);
45  else
46  testCursor.setPosition(testCursor.position());
47  testCursor.movePosition(PreviousCharacter, KeepAnchor);
48  return testCursor.selectedText();
49 }
50 
51 void TextCursor::moveWordStart(MoveMode moveMode)
52 {
53  movePosition(StartOfWord, moveMode);
54  qDebug() << "Start: " << position() << ": " << prevChar(2) << prevChar() << "|" << nextChar();
55  // If we are in front of a quote...
56  if(nextChar() == "'"){
57  // If the previous char is alphanumeric, move left one word, otherwise move right one char
58  if(prevChar().contains(m_wordRegEx)){
59  movePosition(WordLeft, moveMode);
60  }else{
61  movePosition(NextCharacter, moveMode);
62  }
63  }
64  // If the previous char is a quote, and the char before that is alphanumeric, move left one word
65  else if(prevChar() == "'" && prevChar(2).contains(m_wordRegEx)){
66  movePosition(WordLeft, moveMode, 2); // 2: because quote counts as a word boundary
67  }
68 }
69 
70 void TextCursor::moveWordEnd(MoveMode moveMode)
71 {
72  movePosition(EndOfWord, moveMode);
73  qDebug() << "End: " << position() << ": " << prevChar() << " | " << nextChar() << "|" << nextChar(2);
74  // If we are in behind of a quote...
75  if(prevChar() == "'"){
76  // If the next char is alphanumeric, move right one word, otherwise move left one char
77  if(nextChar().contains(m_wordRegEx)){
78  movePosition(WordRight, moveMode);
79  }else{
80  movePosition(PreviousCharacter, moveMode);
81  }
82  }
83  // If the next char is a quote, and the char after that is alphanumeric, move right one word
84  else if(nextChar() == "'" && nextChar(2).contains(m_wordRegEx)){
85  movePosition(WordRight, moveMode, 2); // 2: because quote counts as a word boundary
86  }
87 }
88 
90 
92  : Checker(parent)
93 {
94  m_textEdit = 0;
95  m_document = 0;
96  m_undoRedoStack = 0;
97  m_undoRedoInProgress = false;
98 }
99 
101 {
102  setTextEdit(reinterpret_cast<TextEditProxy*>(0));
103 }
104 
105 void TextEditChecker::setTextEdit(QTextEdit* textEdit)
106 {
107  setTextEdit(textEdit ? new TextEditProxyT<QTextEdit>(textEdit) : reinterpret_cast<TextEditProxyT<QTextEdit>*>(0));
108 }
109 
110 void TextEditChecker::setTextEdit(QPlainTextEdit* textEdit)
111 {
112  setTextEdit(textEdit ? new TextEditProxyT<QPlainTextEdit>(textEdit) : reinterpret_cast<TextEditProxyT<QPlainTextEdit>*>(0));
113 }
114 
115 void TextEditChecker::setTextEdit(TextEditProxy *textEdit)
116 {
117  if(!textEdit && m_textEdit){
118  disconnect(m_textEdit->object(), SIGNAL(destroyed()), this, SLOT(slotDetachTextEdit()));
119  disconnect(m_textEdit->object(), SIGNAL(textChanged()), this, SLOT(slotCheckDocumentChanged()));
120  disconnect(m_textEdit->object(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowContextMenu(QPoint)));
121  disconnect(m_textEdit->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
122  m_textEdit->setContextMenuPolicy(m_oldContextMenuPolicy);
123  m_textEdit->removeEventFilter(this);
124 
125  // Remove spelling format
126  QTextCursor cursor = m_textEdit->textCursor();
127  cursor.movePosition(QTextCursor::Start);
128  cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
129  QTextCharFormat fmt = cursor.charFormat();
130  QTextCharFormat defaultFormat = QTextCharFormat();
131  fmt.setFontUnderline(defaultFormat.fontUnderline());
132  fmt.setUnderlineColor(defaultFormat.underlineColor());
133  fmt.setUnderlineStyle(defaultFormat.underlineStyle());
134  cursor.setCharFormat(fmt);
135  }
136  bool undoWasEnabled = m_undoRedoStack != 0;
137  setUndoRedoEnabled(false);
138  delete m_textEdit;
139  m_document = 0;
140  m_textEdit = textEdit;
141  if(m_textEdit){
142  m_document = m_textEdit->document();
143  connect(m_textEdit->object(), SIGNAL(destroyed()), this, SLOT(slotDetachTextEdit()));
144  connect(m_textEdit->object(), SIGNAL(textChanged()), this, SLOT(slotCheckDocumentChanged()));
145  connect(m_textEdit->object(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowContextMenu(QPoint)));
146  connect(m_textEdit->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
147  m_oldContextMenuPolicy = m_textEdit->contextMenuPolicy();
148  setUndoRedoEnabled(undoWasEnabled);
149  m_textEdit->setContextMenuPolicy(Qt::CustomContextMenu);
150  m_textEdit->installEventFilter(this);
151  checkSpelling();
152  }
153 }
154 
155 bool TextEditChecker::eventFilter(QObject* obj, QEvent* event)
156 {
157  if(event->type() == QEvent::KeyPress){
158  QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
159  if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == Qt::CTRL){
160  undo();
161  return true;
162  }else if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == (Qt::CTRL | Qt::SHIFT)){
163  redo();
164  return true;
165  }
166  }
167  return QObject::eventFilter(obj, event);
168 }
169 
170 void TextEditChecker::checkSpelling(int start, int end)
171 {
172  if(end == -1){
173  QTextCursor tmpCursor(m_textEdit->textCursor());
174  tmpCursor.movePosition(QTextCursor::End);
175  end = tmpCursor.position();
176  }
177 
178  // stop contentsChange signals from being emitted due to changed charFormats
179  m_textEdit->document()->blockSignals(true);
180 
181  qDebug() << "Checking range " << start << " - " << end;
182 
183  QTextCharFormat errorFmt;
184  errorFmt.setFontUnderline(true);
185  errorFmt.setUnderlineColor(Qt::red);
186  errorFmt.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
187  QTextCharFormat defaultFormat = QTextCharFormat();
188 
189  TextCursor cursor(m_textEdit->textCursor());
190  cursor.beginEditBlock();
191  cursor.setPosition(start);
192  while(cursor.position() < end) {
193  cursor.moveWordEnd(QTextCursor::KeepAnchor);
194  QString word = cursor.selectedText();
195  bool correct = checkWord(word);
196  qDebug() << "Checking word:" << word << "(" << cursor.anchor() << "-" << cursor.position() << "), correct:" << correct;
197  if(!correct){
198  cursor.mergeCharFormat(errorFmt);
199  }else{
200  QTextCharFormat fmt = cursor.charFormat();
201  fmt.setFontUnderline(defaultFormat.fontUnderline());
202  fmt.setUnderlineColor(defaultFormat.underlineColor());
203  fmt.setUnderlineStyle(defaultFormat.underlineStyle());
204  cursor.setCharFormat(fmt);
205  }
206  // Go to next word start
207  while(cursor.position() < end && !cursor.isWordChar(cursor.nextChar())){
208  cursor.movePosition(QTextCursor::NextCharacter);
209  }
210  }
211  cursor.endEditBlock();
212 
213  m_textEdit->document()->blockSignals(false);
214 }
215 
217 {
218  if(m_undoRedoStack){
219  m_undoRedoStack->clear();
220  }
221 }
222 
224 {
225  if(enabled == (m_undoRedoStack != 0)){
226  return;
227  }
228  if(!enabled){
229  delete m_undoRedoStack;
230  m_undoRedoStack = 0;
231  emit undoAvailable(false);
232  emit redoAvailable(false);
233  }else{
234  m_undoRedoStack = new UndoRedoStack(m_textEdit);
235  connect(m_undoRedoStack, SIGNAL(undoAvailable(bool)), this, SIGNAL(undoAvailable(bool)));
236  connect(m_undoRedoStack, SIGNAL(redoAvailable(bool)), this, SIGNAL(redoAvailable(bool)));
237  }
238 }
239 
240 QString TextEditChecker::getWord(int pos, int* start, int* end) const
241 {
242  TextCursor cursor(m_textEdit->textCursor());
243  cursor.setPosition(pos);
244  cursor.moveWordStart();
245  cursor.moveWordEnd(QTextCursor::KeepAnchor);
246  if(start)
247  *start = cursor.anchor();
248  if(end)
249  *end = cursor.position();
250  return cursor.selectedText();
251 }
252 
253 void TextEditChecker::insertWord(int start, int end, const QString &word)
254 {
255  QTextCursor cursor(m_textEdit->textCursor());
256  cursor.setPosition(start);
257  cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, end - start);
258  cursor.insertText(word);
259 }
260 
261 void TextEditChecker::slotShowContextMenu(const QPoint &pos)
262 {
263  QPoint globalPos = m_textEdit->mapToGlobal(pos);
264  QMenu* menu = m_textEdit->createStandardContextMenu();
265  int wordPos = m_textEdit->cursorForPosition(pos).position();
266  showContextMenu(menu, globalPos, wordPos);
267 }
268 
269 void TextEditChecker::slotCheckDocumentChanged()
270 {
271  if(m_document != m_textEdit->document()) {
272  bool undoWasEnabled = m_undoRedoStack != 0;
273  setUndoRedoEnabled(false);
274  if(m_document){
275  disconnect(m_document, SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
276  }
277  m_document = m_textEdit->document();
278  connect(m_document, SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
279  setUndoRedoEnabled(undoWasEnabled);
280  }
281 }
282 
283 void TextEditChecker::slotDetachTextEdit()
284 {
285  bool undoWasEnabled = m_undoRedoStack != 0;
286  setUndoRedoEnabled(false);
287  // Signals are disconnected when objects are deleted
288  delete m_textEdit;
289  m_textEdit = 0;
290  m_document = 0;
291  if(undoWasEnabled){
292  // Crate dummy instance
293  setUndoRedoEnabled(true);
294  }
295 }
296 
297 void TextEditChecker::slotCheckRange(int pos, int removed, int added)
298 {
299  if(m_undoRedoStack != 0 && !m_undoRedoInProgress){
300  m_undoRedoStack->handleContentsChange(pos, removed, added);
301  }
302 
303  // Qt Bug? Apparently, when contents is pasted at pos = 0, added and removed are too large by 1
304  TextCursor c(m_textEdit->textCursor());
305  c.movePosition(QTextCursor::End);
306  int len = c.position();
307  if(pos == 0 && added > len){
308  --added;
309  }
310 
311  // Set default format on inserted text
312  c.beginEditBlock();
313  c.setPosition(pos);
314  c.moveWordStart();
315  c.setPosition(pos + added, QTextCursor::KeepAnchor);
316  c.moveWordEnd(QTextCursor::KeepAnchor);
317  QTextCharFormat fmt = c.charFormat();
318  QTextCharFormat defaultFormat = QTextCharFormat();
319  fmt.setFontUnderline(defaultFormat.fontUnderline());
320  fmt.setUnderlineColor(defaultFormat.underlineColor());
321  fmt.setUnderlineStyle(defaultFormat.underlineStyle());
322  c.setCharFormat(fmt);
323  checkSpelling(c.anchor(), c.position());
324  c.endEditBlock();
325 }
326 
328 {
329  if(m_undoRedoStack != 0){
330  m_undoRedoInProgress = true;
331  m_undoRedoStack->undo();
332  m_textEdit->ensureCursorVisible();
333  m_undoRedoInProgress = false;
334  }
335 }
336 
338 {
339  if(m_undoRedoStack != 0){
340  m_undoRedoInProgress = true;
341  m_undoRedoStack->redo();
342  m_textEdit->ensureCursorVisible();
343  m_undoRedoInProgress = false;
344  }
345 }
346 
347 } // QtSpell
QString prevChar(int num=1) const
Retreive the num-th previous character.
void undo()
Undo the last edit operation.
TextEditChecker(QObject *parent=0)
TextEditChecker object constructor.
void clearUndoRedo()
Clears the undo/redo stack.
void setTextEdit(QTextEdit *textEdit)
Set the QTextEdit to check.
void setUndoRedoEnabled(bool enabled)
Sets whether undo/redo functionality is enabled.
void redoAvailable(bool available)
Emitted when the redo stak changes.
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.
void checkSpelling(int start=0, int end=-1)
Check the spelling.
QString nextChar(int num=1) const
Retreive the num-th next character.
bool checkWord(const QString &word) const
Check the specified word.
Definition: Checker.cpp:121
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...
void moveWordStart(MoveMode moveMode=MoveAnchor)
Move the cursor to the start of the current word. Cursor must be inside a word. This method correctly...
void redo()
Redo the last edit operation.
An enhanced QTextCursor.
An abstract class providing spell checking support.
Definition: QtSpell.hpp:45
~TextEditChecker()
TextEditChecker object destructor.