|
QtSpell
0.8.2
Spell checking for Qt text widgets
|
00001 /* QtSpell - Spell checking for Qt text widgets. 00002 * Copyright (c) 2014 Sandro Mani 00003 * 00004 * This program is free software; you can redistribute it and/or modify 00005 * it under the terms of the GNU General Public License as published by 00006 * the Free Software Foundation; either version 2 of the License, or 00007 * (at your option) any later version. 00008 * 00009 * This program is distributed in the hope that it will be useful, 00010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 * GNU General Public License for more details. 00013 * 00014 * You should have received a copy of the GNU General Public License along 00015 * with this program; if not, write to the Free Software Foundation, Inc., 00016 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 00017 */ 00018 00019 #include "QtSpell.hpp" 00020 #include "TextEditChecker_p.hpp" 00021 #include "UndoRedoStack.hpp" 00022 00023 #include <QDebug> 00024 #include <QPlainTextEdit> 00025 #include <QTextEdit> 00026 #include <QTextBlock> 00027 00028 namespace QtSpell { 00029 00030 QString TextCursor::nextChar(int num) const 00031 { 00032 TextCursor testCursor(*this); 00033 if(num > 1) 00034 testCursor.movePosition(NextCharacter, MoveAnchor, num - 1); 00035 else 00036 testCursor.setPosition(testCursor.position()); 00037 testCursor.movePosition(NextCharacter, KeepAnchor); 00038 return testCursor.selectedText(); 00039 } 00040 00041 QString TextCursor::prevChar(int num) const 00042 { 00043 TextCursor testCursor(*this); 00044 if(num > 1) 00045 testCursor.movePosition(PreviousCharacter, MoveAnchor, num - 1); 00046 else 00047 testCursor.setPosition(testCursor.position()); 00048 testCursor.movePosition(PreviousCharacter, KeepAnchor); 00049 return testCursor.selectedText(); 00050 } 00051 00052 void TextCursor::moveWordStart(MoveMode moveMode) 00053 { 00054 movePosition(StartOfWord, moveMode); 00055 qDebug() << "Start: " << position() << ": " << prevChar(2) << prevChar() << "|" << nextChar(); 00056 // If we are in front of a quote... 00057 if(nextChar() == "'"){ 00058 // If the previous char is alphanumeric, move left one word, otherwise move right one char 00059 if(prevChar().contains(m_wordRegEx)){ 00060 movePosition(WordLeft, moveMode); 00061 }else{ 00062 movePosition(NextCharacter, moveMode); 00063 } 00064 } 00065 // If the previous char is a quote, and the char before that is alphanumeric, move left one word 00066 else if(prevChar() == "'" && prevChar(2).contains(m_wordRegEx)){ 00067 movePosition(WordLeft, moveMode, 2); // 2: because quote counts as a word boundary 00068 } 00069 } 00070 00071 void TextCursor::moveWordEnd(MoveMode moveMode) 00072 { 00073 movePosition(EndOfWord, moveMode); 00074 qDebug() << "End: " << position() << ": " << prevChar() << " | " << nextChar() << "|" << nextChar(2); 00075 // If we are in behind of a quote... 00076 if(prevChar() == "'"){ 00077 // If the next char is alphanumeric, move right one word, otherwise move left one char 00078 if(nextChar().contains(m_wordRegEx)){ 00079 movePosition(WordRight, moveMode); 00080 }else{ 00081 movePosition(PreviousCharacter, moveMode); 00082 } 00083 } 00084 // If the next char is a quote, and the char after that is alphanumeric, move right one word 00085 else if(nextChar() == "'" && nextChar(2).contains(m_wordRegEx)){ 00086 movePosition(WordRight, moveMode, 2); // 2: because quote counts as a word boundary 00087 } 00088 } 00089 00091 00092 TextEditChecker::TextEditChecker(QObject* parent) 00093 : Checker(parent) 00094 { 00095 m_textEdit = 0; 00096 m_document = 0; 00097 m_undoRedoStack = 0; 00098 m_undoRedoInProgress = false; 00099 m_noSpellingProperty = -1; 00100 } 00101 00102 TextEditChecker::~TextEditChecker() 00103 { 00104 setTextEdit(reinterpret_cast<TextEditProxy*>(0)); 00105 } 00106 00107 void TextEditChecker::setTextEdit(QTextEdit* textEdit) 00108 { 00109 setTextEdit(textEdit ? new TextEditProxyT<QTextEdit>(textEdit) : reinterpret_cast<TextEditProxyT<QTextEdit>*>(0)); 00110 } 00111 00112 void TextEditChecker::setTextEdit(QPlainTextEdit* textEdit) 00113 { 00114 setTextEdit(textEdit ? new TextEditProxyT<QPlainTextEdit>(textEdit) : reinterpret_cast<TextEditProxyT<QPlainTextEdit>*>(0)); 00115 } 00116 00117 void TextEditChecker::setTextEdit(TextEditProxy *textEdit) 00118 { 00119 if(!textEdit && m_textEdit){ 00120 disconnect(m_textEdit->object(), SIGNAL(destroyed()), this, SLOT(slotDetachTextEdit())); 00121 disconnect(m_textEdit->object(), SIGNAL(textChanged()), this, SLOT(slotCheckDocumentChanged())); 00122 disconnect(m_textEdit->object(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowContextMenu(QPoint))); 00123 disconnect(m_textEdit->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int))); 00124 m_textEdit->setContextMenuPolicy(m_oldContextMenuPolicy); 00125 m_textEdit->removeEventFilter(this); 00126 00127 // Remove spelling format 00128 QTextCursor cursor = m_textEdit->textCursor(); 00129 cursor.movePosition(QTextCursor::Start); 00130 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); 00131 QTextCharFormat fmt = cursor.charFormat(); 00132 QTextCharFormat defaultFormat = QTextCharFormat(); 00133 fmt.setFontUnderline(defaultFormat.fontUnderline()); 00134 fmt.setUnderlineColor(defaultFormat.underlineColor()); 00135 fmt.setUnderlineStyle(defaultFormat.underlineStyle()); 00136 cursor.setCharFormat(fmt); 00137 } 00138 bool undoWasEnabled = m_undoRedoStack != 0; 00139 setUndoRedoEnabled(false); 00140 delete m_textEdit; 00141 m_document = 0; 00142 m_textEdit = textEdit; 00143 if(m_textEdit){ 00144 m_document = m_textEdit->document(); 00145 connect(m_textEdit->object(), SIGNAL(destroyed()), this, SLOT(slotDetachTextEdit())); 00146 connect(m_textEdit->object(), SIGNAL(textChanged()), this, SLOT(slotCheckDocumentChanged())); 00147 connect(m_textEdit->object(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowContextMenu(QPoint))); 00148 connect(m_textEdit->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int))); 00149 m_oldContextMenuPolicy = m_textEdit->contextMenuPolicy(); 00150 setUndoRedoEnabled(undoWasEnabled); 00151 m_textEdit->setContextMenuPolicy(Qt::CustomContextMenu); 00152 m_textEdit->installEventFilter(this); 00153 checkSpelling(); 00154 } 00155 } 00156 00157 bool TextEditChecker::eventFilter(QObject* obj, QEvent* event) 00158 { 00159 if(event->type() == QEvent::KeyPress){ 00160 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); 00161 if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == Qt::CTRL){ 00162 undo(); 00163 return true; 00164 }else if(keyEvent->key() == Qt::Key_Z && keyEvent->modifiers() == (Qt::CTRL | Qt::SHIFT)){ 00165 redo(); 00166 return true; 00167 } 00168 } 00169 return QObject::eventFilter(obj, event); 00170 } 00171 00172 void TextEditChecker::checkSpelling(int start, int end) 00173 { 00174 if(end == -1){ 00175 QTextCursor tmpCursor(m_textEdit->textCursor()); 00176 tmpCursor.movePosition(QTextCursor::End); 00177 end = tmpCursor.position(); 00178 } 00179 00180 // stop contentsChange signals from being emitted due to changed charFormats 00181 m_textEdit->document()->blockSignals(true); 00182 00183 qDebug() << "Checking range " << start << " - " << end; 00184 00185 QTextCharFormat errorFmt; 00186 errorFmt.setFontUnderline(true); 00187 errorFmt.setUnderlineColor(Qt::red); 00188 errorFmt.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); 00189 QTextCharFormat defaultFormat = QTextCharFormat(); 00190 00191 TextCursor cursor(m_textEdit->textCursor()); 00192 cursor.beginEditBlock(); 00193 cursor.setPosition(start); 00194 while(cursor.position() < end) { 00195 cursor.moveWordEnd(QTextCursor::KeepAnchor); 00196 bool correct; 00197 QString word = cursor.selectedText(); 00198 if(noSpellingPropertySet(cursor)) { 00199 correct = true; 00200 qDebug() << "Skipping word:" << word << "(" << cursor.anchor() << "-" << cursor.position() << ")"; 00201 } else { 00202 correct = checkWord(word); 00203 qDebug() << "Checking word:" << word << "(" << cursor.anchor() << "-" << cursor.position() << "), correct:" << correct; 00204 } 00205 if(!correct){ 00206 cursor.mergeCharFormat(errorFmt); 00207 }else{ 00208 QTextCharFormat fmt = cursor.charFormat(); 00209 fmt.setFontUnderline(defaultFormat.fontUnderline()); 00210 fmt.setUnderlineColor(defaultFormat.underlineColor()); 00211 fmt.setUnderlineStyle(defaultFormat.underlineStyle()); 00212 cursor.setCharFormat(fmt); 00213 } 00214 // Go to next word start 00215 while(cursor.position() < end && !cursor.isWordChar(cursor.nextChar())){ 00216 cursor.movePosition(QTextCursor::NextCharacter); 00217 } 00218 } 00219 cursor.endEditBlock(); 00220 00221 m_textEdit->document()->blockSignals(false); 00222 } 00223 00224 bool TextEditChecker::noSpellingPropertySet(const QTextCursor &cursor) const 00225 { 00226 if(m_noSpellingProperty < QTextFormat::UserProperty) { 00227 return false; 00228 } 00229 if(cursor.charFormat().intProperty(m_noSpellingProperty) == 1) { 00230 return true; 00231 } 00232 const QList<QTextLayout::FormatRange>& formats = cursor.block().layout()->additionalFormats(); 00233 int pos = cursor.positionInBlock(); 00234 foreach(const QTextLayout::FormatRange& range, formats) { 00235 if(pos > range.start && pos <= range.start + range.length && range.format.intProperty(m_noSpellingProperty) == 1) { 00236 return true; 00237 } 00238 } 00239 return false; 00240 } 00241 00242 void TextEditChecker::clearUndoRedo() 00243 { 00244 if(m_undoRedoStack){ 00245 m_undoRedoStack->clear(); 00246 } 00247 } 00248 00249 void TextEditChecker::setUndoRedoEnabled(bool enabled) 00250 { 00251 if(enabled == (m_undoRedoStack != 0)){ 00252 return; 00253 } 00254 if(!enabled){ 00255 delete m_undoRedoStack; 00256 m_undoRedoStack = 0; 00257 emit undoAvailable(false); 00258 emit redoAvailable(false); 00259 }else{ 00260 m_undoRedoStack = new UndoRedoStack(m_textEdit); 00261 connect(m_undoRedoStack, SIGNAL(undoAvailable(bool)), this, SIGNAL(undoAvailable(bool))); 00262 connect(m_undoRedoStack, SIGNAL(redoAvailable(bool)), this, SIGNAL(redoAvailable(bool))); 00263 } 00264 } 00265 00266 QString TextEditChecker::getWord(int pos, int* start, int* end) const 00267 { 00268 TextCursor cursor(m_textEdit->textCursor()); 00269 cursor.setPosition(pos); 00270 cursor.moveWordStart(); 00271 cursor.moveWordEnd(QTextCursor::KeepAnchor); 00272 if(start) 00273 *start = cursor.anchor(); 00274 if(end) 00275 *end = cursor.position(); 00276 return cursor.selectedText(); 00277 } 00278 00279 void TextEditChecker::insertWord(int start, int end, const QString &word) 00280 { 00281 QTextCursor cursor(m_textEdit->textCursor()); 00282 cursor.setPosition(start); 00283 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, end - start); 00284 cursor.insertText(word); 00285 } 00286 00287 void TextEditChecker::slotShowContextMenu(const QPoint &pos) 00288 { 00289 QPoint globalPos = m_textEdit->mapToGlobal(pos); 00290 QMenu* menu = m_textEdit->createStandardContextMenu(); 00291 int wordPos = m_textEdit->cursorForPosition(pos).position(); 00292 showContextMenu(menu, globalPos, wordPos); 00293 } 00294 00295 void TextEditChecker::slotCheckDocumentChanged() 00296 { 00297 if(m_document != m_textEdit->document()) { 00298 bool undoWasEnabled = m_undoRedoStack != 0; 00299 setUndoRedoEnabled(false); 00300 if(m_document){ 00301 disconnect(m_document, SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int))); 00302 } 00303 m_document = m_textEdit->document(); 00304 connect(m_document, SIGNAL(contentsChange(int,int,int)), this, SLOT(slotCheckRange(int,int,int))); 00305 setUndoRedoEnabled(undoWasEnabled); 00306 } 00307 } 00308 00309 void TextEditChecker::slotDetachTextEdit() 00310 { 00311 bool undoWasEnabled = m_undoRedoStack != 0; 00312 setUndoRedoEnabled(false); 00313 // Signals are disconnected when objects are deleted 00314 delete m_textEdit; 00315 m_textEdit = 0; 00316 m_document = 0; 00317 if(undoWasEnabled){ 00318 // Crate dummy instance 00319 setUndoRedoEnabled(true); 00320 } 00321 } 00322 00323 void TextEditChecker::slotCheckRange(int pos, int removed, int added) 00324 { 00325 if(m_undoRedoStack != 0 && !m_undoRedoInProgress){ 00326 m_undoRedoStack->handleContentsChange(pos, removed, added); 00327 } 00328 00329 // Qt Bug? Apparently, when contents is pasted at pos = 0, added and removed are too large by 1 00330 TextCursor c(m_textEdit->textCursor()); 00331 c.movePosition(QTextCursor::End); 00332 int len = c.position(); 00333 if(pos == 0 && added > len){ 00334 --added; 00335 } 00336 00337 // Set default format on inserted text 00338 c.beginEditBlock(); 00339 c.setPosition(pos); 00340 c.moveWordStart(); 00341 c.setPosition(pos + added, QTextCursor::KeepAnchor); 00342 c.moveWordEnd(QTextCursor::KeepAnchor); 00343 QTextCharFormat fmt = c.charFormat(); 00344 QTextCharFormat defaultFormat = QTextCharFormat(); 00345 fmt.setFontUnderline(defaultFormat.fontUnderline()); 00346 fmt.setUnderlineColor(defaultFormat.underlineColor()); 00347 fmt.setUnderlineStyle(defaultFormat.underlineStyle()); 00348 c.setCharFormat(fmt); 00349 checkSpelling(c.anchor(), c.position()); 00350 c.endEditBlock(); 00351 } 00352 00353 void TextEditChecker::undo() 00354 { 00355 if(m_undoRedoStack != 0){ 00356 m_undoRedoInProgress = true; 00357 m_undoRedoStack->undo(); 00358 m_textEdit->ensureCursorVisible(); 00359 m_undoRedoInProgress = false; 00360 } 00361 } 00362 00363 void TextEditChecker::redo() 00364 { 00365 if(m_undoRedoStack != 0){ 00366 m_undoRedoInProgress = true; 00367 m_undoRedoStack->redo(); 00368 m_textEdit->ensureCursorVisible(); 00369 m_undoRedoInProgress = false; 00370 } 00371 } 00372 00373 } // QtSpell
1.7.6.1