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
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:266
QtSpell
QtSpell namespace.
Definition: Checker.cpp:65
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:242
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:279
QtSpell::Checker::checkWord
bool checkWord(const QString &word) const
Check the specified word.
Definition: Checker.cpp:135
QtSpell::TextEditChecker::undo
void undo()
Undo the last edit operation.
Definition: TextEditChecker.cpp:353
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:58
QtSpell::TextEditChecker::redo
void redo()
Redo the last edit operation.
Definition: TextEditChecker.cpp:363
QtSpell::TextEditChecker::setUndoRedoEnabled
void setUndoRedoEnabled(bool enabled)
Sets whether undo/redo functionality is enabled.
Definition: TextEditChecker.cpp:249
QtSpell::TextCursor
An enhanced QTextCursor.
Definition: TextEditChecker_p.hpp:32
QtSpell::TextEditChecker::checkSpelling
void checkSpelling(int start=0, int end=-1)
Check the spelling.
Definition: TextEditChecker.cpp:172
QtSpell::TextEditChecker::~TextEditChecker
~TextEditChecker()
TextEditChecker object destructor.
Definition: TextEditChecker.cpp:102
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:107