QtSpell  0.8.2
Spell checking for Qt text widgets
/usr/src/RPM/BUILD/qtspell-0.8.2/src/TextEditChecker.cpp
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