QtSpell  0.7.2
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  cursor.setCharFormat(QTextCharFormat());
130  }
131  bool undoWasEnabled = m_undoRedoStack != 0;
132  setUndoRedoEnabled(false);
133  delete m_textEdit;
134  m_document = 0;
135  m_textEdit = textEdit;
136  if(m_textEdit){
137  m_document = m_textEdit->document();
138  connect(m_textEdit->object(), SIGNAL(destroyed()), this, SLOT(slotDetachTextEdit()));
139  connect(m_textEdit->object(), SIGNAL(textChanged()), this, SLOT(slotCheckDocumentChanged()));
140  connect(m_textEdit->object(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowContextMenu(QPoint)));
141  connect(m_textEdit->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
142  m_oldContextMenuPolicy = m_textEdit->contextMenuPolicy();
143  setUndoRedoEnabled(undoWasEnabled);
144  m_textEdit->setContextMenuPolicy(Qt::CustomContextMenu);
145  m_textEdit->installEventFilter(this);
146  checkSpelling();
147  }
148 }
149 
150 bool TextEditChecker::eventFilter(QObject* obj, QEvent* event)
151 {
152  if(event->type() == QEvent::KeyPress){
153  QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
154  if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == Qt::CTRL){
155  undo();
156  return true;
157  }else if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == (Qt::CTRL | Qt::SHIFT)){
158  redo();
159  return true;
160  }
161  }
162  return QObject::eventFilter(obj, event);
163 }
164 
165 void TextEditChecker::checkSpelling(int start, int end)
166 {
167  if(end == -1){
168  QTextCursor tmpCursor(m_textEdit->textCursor());
169  tmpCursor.movePosition(QTextCursor::End);
170  end = tmpCursor.position();
171  }
172 
173  // stop contentsChange signals from being emitted due to changed charFormats
174  m_textEdit->document()->blockSignals(true);
175 
176  qDebug() << "Checking range " << start << " - " << end;
177 
178  QTextCharFormat errorFmt;
179  errorFmt.setFontUnderline(true);
180  errorFmt.setUnderlineColor(Qt::red);
181  errorFmt.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
182  QTextCharFormat defaultFmt;
183 
184  TextCursor cursor(m_textEdit->textCursor());
185  cursor.beginEditBlock();
186  cursor.setPosition(start);
187  while(cursor.position() < end) {
188  cursor.moveWordEnd(QTextCursor::KeepAnchor);
189  QString word = cursor.selectedText();
190  bool correct = checkWord(word);
191  qDebug() << "Checking word:" << word << "(" << cursor.anchor() << "-" << cursor.position() << "), correct:" << correct;
192  if(!correct){
193  cursor.setCharFormat(errorFmt);
194  }else{
195  cursor.setCharFormat(defaultFmt);
196  }
197  // Go to next word start
198  while(cursor.position() < end && !cursor.isWordChar(cursor.nextChar())){
199  cursor.movePosition(QTextCursor::NextCharacter);
200  }
201  }
202  cursor.endEditBlock();
203 
204  m_textEdit->document()->blockSignals(false);
205 }
206 
208 {
209  if(m_undoRedoStack){
210  m_undoRedoStack->clear();
211  }
212 }
213 
215 {
216  if(enabled == (m_undoRedoStack != 0)){
217  return;
218  }
219  if(!enabled){
220  delete m_undoRedoStack;
221  m_undoRedoStack = 0;
222  emit undoAvailable(false);
223  emit redoAvailable(false);
224  }else{
225  m_undoRedoStack = new UndoRedoStack(m_textEdit);
226  connect(m_undoRedoStack, SIGNAL(undoAvailable(bool)), this, SIGNAL(undoAvailable(bool)));
227  connect(m_undoRedoStack, SIGNAL(redoAvailable(bool)), this, SIGNAL(redoAvailable(bool)));
228  }
229 }
230 
231 QString TextEditChecker::getWord(int pos, int* start, int* end) const
232 {
233  TextCursor cursor(m_textEdit->textCursor());
234  cursor.setPosition(pos);
235  cursor.moveWordStart();
236  cursor.moveWordEnd(QTextCursor::KeepAnchor);
237  if(start)
238  *start = cursor.anchor();
239  if(end)
240  *end = cursor.position();
241  return cursor.selectedText();
242 }
243 
244 void TextEditChecker::insertWord(int start, int end, const QString &word)
245 {
246  QTextCursor cursor(m_textEdit->textCursor());
247  cursor.setPosition(start);
248  cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, end - start);
249  cursor.insertText(word);
250 }
251 
252 void TextEditChecker::slotShowContextMenu(const QPoint &pos)
253 {
254  QPoint globalPos = m_textEdit->mapToGlobal(pos);
255  QMenu* menu = m_textEdit->createStandardContextMenu();
256  int wordPos = m_textEdit->cursorForPosition(pos).position();
257  showContextMenu(menu, globalPos, wordPos);
258 }
259 
260 void TextEditChecker::slotCheckDocumentChanged()
261 {
262  if(m_document != m_textEdit->document()) {
263  bool undoWasEnabled = m_undoRedoStack != 0;
264  setUndoRedoEnabled(false);
265  if(m_document){
266  disconnect(m_document, SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
267  }
268  m_document = m_textEdit->document();
269  connect(m_document, SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int)));
270  setUndoRedoEnabled(undoWasEnabled);
271  }
272 }
273 
274 void TextEditChecker::slotDetachTextEdit()
275 {
276  bool undoWasEnabled = m_undoRedoStack != 0;
277  setUndoRedoEnabled(false);
278  // Signals are disconnected when objects are deleted
279  delete m_textEdit;
280  m_textEdit = 0;
281  m_document = 0;
282  if(undoWasEnabled){
283  // Crate dummy instance
284  setUndoRedoEnabled(true);
285  }
286 }
287 
288 void TextEditChecker::slotCheckRange(int pos, int removed, int added)
289 {
290  if(m_undoRedoStack != 0 && !m_undoRedoInProgress){
291  m_undoRedoStack->handleContentsChange(pos, removed, added);
292  }
293 
294  // Qt Bug? Apparently, when contents is pasted at pos = 0, added and removed are too large by 1
295  TextCursor c(m_textEdit->textCursor());
296  c.movePosition(QTextCursor::End);
297  int len = c.position();
298  if(pos == 0 && added > len){
299  --added;
300  }
301 
302  // Set default format on inserted text
303  c.beginEditBlock();
304  c.setPosition(pos);
305  c.moveWordStart();
306  c.setPosition(pos + added, QTextCursor::KeepAnchor);
307  c.moveWordEnd(QTextCursor::KeepAnchor);
308  c.setCharFormat(QTextCharFormat());
309  checkSpelling(c.anchor(), c.position());
310  c.endEditBlock();
311 }
312 
314 {
315  if(m_undoRedoStack != 0){
316  m_undoRedoInProgress = true;
317  m_undoRedoStack->undo();
318  m_textEdit->ensureCursorVisible();
319  m_undoRedoInProgress = false;
320  }
321 }
322 
324 {
325  if(m_undoRedoStack != 0){
326  m_undoRedoInProgress = true;
327  m_undoRedoStack->redo();
328  m_textEdit->ensureCursorVisible();
329  m_undoRedoInProgress = false;
330  }
331 }
332 
333 } // 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:122
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.