KPIMTextedit Library
textedit.cpp
00001 /* 00002 Copyright (c) 2009 Thomas McGuire <mcguire@kde.org> 00003 00004 Based on KMail and libkdepim code by: 00005 Copyright 2007 Laurent Montel <montel@kde.org> 00006 00007 This library is free software; you can redistribute it and/or modify it 00008 under the terms of the GNU Library General Public License as published by 00009 the Free Software Foundation; either version 2 of the License, or (at your 00010 option) any later version. 00011 00012 This library is distributed in the hope that it will be useful, but WITHOUT 00013 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 00014 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 00015 License for more details. 00016 00017 You should have received a copy of the GNU Library General Public License 00018 along with this library; see the file COPYING.LIB. If not, write to the 00019 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 00020 02110-1301, USA. 00021 */ 00022 #include "textedit.h" 00023 00024 #include "emailquotehighlighter.h" 00025 00026 #include <kmime/kmime_codecs.h> 00027 00028 #include <KDE/KAction> 00029 #include <KDE/KActionCollection> 00030 #include <KDE/KCursor> 00031 #include <KDE/KFileDialog> 00032 #include <KDE/KLocalizedString> 00033 #include <KDE/KMessageBox> 00034 #include <KDE/KPushButton> 00035 #include <KDE/KUrl> 00036 00037 #include <QtCore/QBuffer> 00038 #include <QtCore/QDateTime> 00039 #include <QtCore/QMimeData> 00040 #include <QtCore/QFileInfo> 00041 #include <QtCore/QPointer> 00042 #include <QtGui/QKeyEvent> 00043 #include <QtGui/QTextLayout> 00044 00045 #include "textutils.h" 00046 #include <QPlainTextEdit> 00047 00048 namespace KPIMTextEdit { 00049 00050 class TextEditPrivate 00051 { 00052 public: 00053 00054 TextEditPrivate( TextEdit *parent ) 00055 : actionAddImage( 0 ), 00056 actionDeleteLine( 0 ), 00057 q( parent ), 00058 imageSupportEnabled( false ) 00059 { 00060 } 00061 00070 void addImageHelper( const QString &imageName, const QImage &image ); 00071 00075 QList<QTextImageFormat> embeddedImageFormats() const; 00076 00081 void fixupTextEditString( QString &text ) const; 00082 00086 void init(); 00087 00092 void _k_slotAddImage(); 00093 00094 void _k_slotDeleteLine(); 00095 00097 KAction *actionAddImage; 00098 00100 KAction *actionDeleteLine; 00101 00103 TextEdit *q; 00104 00106 bool imageSupportEnabled; 00107 00113 QStringList mImageNames; 00114 00126 bool spellCheckingEnabled; 00127 00128 QString configFile; 00129 }; 00130 00131 } // namespace 00132 00133 using namespace KPIMTextEdit; 00134 00135 void TextEditPrivate::fixupTextEditString( QString &text ) const 00136 { 00137 // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here 00138 text.remove( QChar::LineSeparator ); 00139 00140 // Get rid of embedded images, see QTextImageFormat documentation: 00141 // "Inline images are represented by an object replacement character (0xFFFC in Unicode) " 00142 text.remove( 0xFFFC ); 00143 00144 // In plaintext mode, each space is non-breaking. 00145 text.replace( QChar::Nbsp, QChar::fromAscii( ' ' ) ); 00146 } 00147 00148 TextEdit::TextEdit( const QString& text, QWidget *parent ) 00149 : KRichTextWidget( text, parent ), 00150 d( new TextEditPrivate( this ) ) 00151 { 00152 d->init(); 00153 } 00154 00155 TextEdit::TextEdit( QWidget *parent ) 00156 : KRichTextWidget( parent ), 00157 d( new TextEditPrivate( this ) ) 00158 { 00159 d->init(); 00160 } 00161 00162 TextEdit::TextEdit( QWidget *parent, const QString& configFile ) 00163 : KRichTextWidget( parent ), 00164 d( new TextEditPrivate( this ) ) 00165 { 00166 d->init(); 00167 d->configFile = configFile; 00168 } 00169 00170 TextEdit::~TextEdit() 00171 { 00172 } 00173 00174 bool TextEdit::eventFilter( QObject*o, QEvent* e ) 00175 { 00176 #ifndef QT_NO_CURSOR 00177 if ( o == this ) 00178 KCursor::autoHideEventFilter( o, e ); 00179 #endif 00180 return KRichTextWidget::eventFilter( o, e ); 00181 } 00182 00183 void TextEditPrivate::init() 00184 { 00185 q->setSpellInterface( q ); 00186 // We tell the KRichTextWidget to enable spell checking, because only then it will 00187 // call createHighlighter() which will create our own highlighter which also 00188 // does quote highlighting. 00189 // However, *our* spellchecking is still disabled. Our own highlighter only 00190 // cares about our spellcheck status, it will not highlight missspelled words 00191 // if our spellchecking is disabled. 00192 // See also KEMailQuotingHighlighter::highlightBlock(). 00193 spellCheckingEnabled = false; 00194 q->setCheckSpellingEnabledInternal( true ); 00195 00196 #ifndef QT_NO_CURSOR 00197 KCursor::setAutoHideCursor( q, true, true ); 00198 #endif 00199 q->installEventFilter( q ); 00200 } 00201 00202 QString TextEdit::configFile() const 00203 { 00204 return d->configFile; 00205 } 00206 00207 00208 void TextEdit::keyPressEvent ( QKeyEvent * e ) 00209 { 00210 if ( e->key() == Qt::Key_Return ) { 00211 QTextCursor cursor = textCursor(); 00212 int oldPos = cursor.position(); 00213 int blockPos = cursor.block().position(); 00214 00215 //selection all the line. 00216 cursor.movePosition( QTextCursor::StartOfBlock ); 00217 cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); 00218 QString lineText = cursor.selectedText(); 00219 if ( ( ( oldPos -blockPos ) > 0 ) && 00220 ( ( oldPos-blockPos ) < int( lineText.length() ) ) ) { 00221 bool isQuotedLine = false; 00222 int bot = 0; // bot = begin of text after quote indicators 00223 while ( bot < lineText.length() ) { 00224 if( ( lineText[bot] == QChar::fromAscii( '>' ) ) || 00225 ( lineText[bot] == QChar::fromAscii( '|' ) ) ) { 00226 isQuotedLine = true; 00227 ++bot; 00228 } 00229 else if ( lineText[bot].isSpace() ) { 00230 ++bot; 00231 } 00232 else { 00233 break; 00234 } 00235 } 00236 KRichTextWidget::keyPressEvent( e ); 00237 // duplicate quote indicators of the previous line before the new 00238 // line if the line actually contained text (apart from the quote 00239 // indicators) and the cursor is behind the quote indicators 00240 if ( isQuotedLine 00241 && ( bot != lineText.length() ) 00242 && ( ( oldPos-blockPos ) >= int( bot ) ) ) { 00243 // The cursor position might have changed unpredictably if there was selected 00244 // text which got replaced by a new line, so we query it again: 00245 cursor.movePosition( QTextCursor::StartOfBlock ); 00246 cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); 00247 QString newLine = cursor.selectedText(); 00248 00249 // remove leading white space from the new line and instead 00250 // add the quote indicators of the previous line 00251 int leadingWhiteSpaceCount = 0; 00252 while ( ( leadingWhiteSpaceCount < newLine.length() ) 00253 && newLine[leadingWhiteSpaceCount].isSpace() ) { 00254 ++leadingWhiteSpaceCount; 00255 } 00256 newLine = newLine.replace( 0, leadingWhiteSpaceCount, 00257 lineText.left( bot ) ); 00258 cursor.insertText( newLine ); 00259 //cursor.setPosition( cursor.position() + 2); 00260 cursor.movePosition( QTextCursor::StartOfBlock ); 00261 setTextCursor( cursor ); 00262 } 00263 } 00264 else 00265 KRichTextWidget::keyPressEvent( e ); 00266 } 00267 else 00268 { 00269 KRichTextWidget::keyPressEvent( e ); 00270 } 00271 } 00272 00273 00274 bool TextEdit::isSpellCheckingEnabled() const 00275 { 00276 return d->spellCheckingEnabled; 00277 } 00278 00279 void TextEdit::setSpellCheckingEnabled( bool enable ) 00280 { 00281 EMailQuoteHighlighter *hlighter = 00282 dynamic_cast<EMailQuoteHighlighter*>( highlighter() ); 00283 if ( hlighter ) 00284 hlighter->toggleSpellHighlighting( enable ); 00285 00286 d->spellCheckingEnabled = enable; 00287 emit checkSpellingChanged( enable ); 00288 } 00289 00290 bool TextEdit::shouldBlockBeSpellChecked( const QString& block ) const 00291 { 00292 return !isLineQuoted( block ); 00293 } 00294 00295 bool KPIMTextEdit::TextEdit::isLineQuoted( const QString& line ) const 00296 { 00297 return quoteLength( line ) > 0; 00298 } 00299 00300 int KPIMTextEdit::TextEdit::quoteLength( const QString& line ) const 00301 { 00302 bool quoteFound = false; 00303 int startOfText = -1; 00304 for ( int i = 0; i < line.length(); i++ ) { 00305 if ( line[i] == QLatin1Char( '>' ) || line[i] == QLatin1Char( '|' ) ) 00306 quoteFound = true; 00307 else if ( line[i] != QLatin1Char( ' ' ) ) { 00308 startOfText = i; 00309 break; 00310 } 00311 } 00312 if ( quoteFound ) { 00313 if ( startOfText == -1 ) 00314 startOfText = line.length() - 1; 00315 return startOfText; 00316 } 00317 else 00318 return 0; 00319 } 00320 00321 const QString KPIMTextEdit::TextEdit::defaultQuoteSign() const 00322 { 00323 return QLatin1String( "> " ); 00324 } 00325 00326 void TextEdit::createHighlighter() 00327 { 00328 EMailQuoteHighlighter *emailHighLighter = 00329 new EMailQuoteHighlighter( this ); 00330 00331 setHighlighterColors( emailHighLighter ); 00332 00333 //TODO change config 00334 KRichTextWidget::setHighlighter( emailHighLighter ); 00335 00336 if ( !spellCheckingLanguage().isEmpty() ) 00337 setSpellCheckingLanguage( spellCheckingLanguage() ); 00338 setSpellCheckingEnabled( isSpellCheckingEnabled() ); 00339 } 00340 00341 void TextEdit::setHighlighterColors( EMailQuoteHighlighter *highlighter ) 00342 { 00343 Q_UNUSED( highlighter ); 00344 } 00345 00346 QString TextEdit::toWrappedPlainText() const 00347 { 00348 QString temp; 00349 QTextDocument* doc = document(); 00350 QTextBlock block = doc->begin(); 00351 while ( block.isValid() ) { 00352 QTextLayout* layout = block.layout(); 00353 for ( int i = 0; i < layout->lineCount(); i++ ) { 00354 QTextLine line = layout->lineAt( i ); 00355 temp += block.text().mid( line.textStart(), line.textLength() ) + QLatin1Char( '\n' ); 00356 } 00357 block = block.next(); 00358 } 00359 00360 // Remove the last superfluous newline added above 00361 if ( temp.endsWith( QLatin1Char( '\n' ) ) ) 00362 temp.chop( 1 ); 00363 00364 d->fixupTextEditString( temp ); 00365 return temp; 00366 } 00367 00368 QString TextEdit::toCleanPlainText() const 00369 { 00370 QString temp = toPlainText(); 00371 d->fixupTextEditString( temp ); 00372 return temp; 00373 } 00374 00375 void TextEdit::createActions( KActionCollection *actionCollection ) 00376 { 00377 KRichTextWidget::createActions( actionCollection ); 00378 00379 if ( d->imageSupportEnabled ) { 00380 d->actionAddImage = new KAction( KIcon( QLatin1String( "insert-image" ) ), 00381 i18n( "Add Image" ), this ); 00382 actionCollection->addAction( QLatin1String( "add_image" ), d->actionAddImage ); 00383 connect( d->actionAddImage, SIGNAL(triggered(bool) ), SLOT( _k_slotAddImage() ) ); 00384 } 00385 00386 d->actionDeleteLine = new KAction( i18n( "Delete Line" ), this ); 00387 d->actionDeleteLine->setShortcut( QKeySequence( Qt::CTRL + Qt::Key_K ) ); 00388 actionCollection->addAction( QLatin1String( "delete_line" ), d->actionDeleteLine ); 00389 connect( d->actionDeleteLine, SIGNAL(triggered(bool)), SLOT(_k_slotDeleteLine()) ); 00390 } 00391 00392 void TextEdit::addImage( const KUrl &url ) 00393 { 00394 QImage image; 00395 if ( !image.load( url.path() ) ) { 00396 KMessageBox::error( this, 00397 i18nc( "@info", "Unable to load image <filename>%1</filename>.", url.path() ) ); 00398 return; 00399 } 00400 QFileInfo fi( url.path() ); 00401 QString imageName = fi.baseName().isEmpty() ? QLatin1String( "image.png" ) 00402 : QString(fi.baseName() + QLatin1String( ".png" )); 00403 d->addImageHelper( imageName, image ); 00404 } 00405 00406 void TextEdit::loadImage ( const QImage& image, const QString& matchName, const QString& resourceName ) 00407 { 00408 QSet<int> cursorPositionsToSkip; 00409 QTextBlock currentBlock = document()->begin(); 00410 QTextBlock::iterator it; 00411 while ( currentBlock.isValid() ) { 00412 for (it = currentBlock.begin(); !(it.atEnd()); ++it) { 00413 QTextFragment fragment = it.fragment(); 00414 if ( fragment.isValid() ) { 00415 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat(); 00416 if ( imageFormat.isValid() && imageFormat.name() == matchName ) { 00417 int pos = fragment.position(); 00418 if ( !cursorPositionsToSkip.contains( pos ) ) { 00419 QTextCursor cursor( document() ); 00420 cursor.setPosition( pos ); 00421 cursor.setPosition( pos + 1, QTextCursor::KeepAnchor ); 00422 cursor.removeSelectedText(); 00423 document()->addResource( QTextDocument::ImageResource, QUrl( resourceName ), QVariant( image ) ); 00424 cursor.insertImage( resourceName ); 00425 00426 // The textfragment iterator is now invalid, restart from the beginning 00427 // Take care not to replace the same fragment again, or we would be in an infinite loop. 00428 cursorPositionsToSkip.insert( pos ); 00429 it = currentBlock.begin(); 00430 } 00431 } 00432 } 00433 } 00434 currentBlock = currentBlock.next(); 00435 } 00436 } 00437 00438 void TextEditPrivate::addImageHelper( const QString &imageName, const QImage &image ) 00439 { 00440 QString imageNameToAdd = imageName; 00441 QTextDocument *document = q->document(); 00442 00443 // determine the imageNameToAdd 00444 int imageNumber = 1; 00445 while ( mImageNames.contains( imageNameToAdd ) ) { 00446 QVariant qv = document->resource( QTextDocument::ImageResource, QUrl( imageNameToAdd ) ); 00447 if ( qv == image ) { 00448 // use the same name 00449 break; 00450 } 00451 int firstDot = imageName.indexOf( QLatin1Char( '.' ) ); 00452 if ( firstDot == -1 ) 00453 imageNameToAdd = imageName + QString::number( imageNumber++ ); 00454 else 00455 imageNameToAdd = imageName.left( firstDot ) + QString::number( imageNumber++ ) + 00456 imageName.mid( firstDot ); 00457 } 00458 00459 if ( !mImageNames.contains( imageNameToAdd ) ) { 00460 document->addResource( QTextDocument::ImageResource, QUrl( imageNameToAdd ), image ); 00461 mImageNames << imageNameToAdd; 00462 } 00463 q->textCursor().insertImage( imageNameToAdd ); 00464 q->enableRichTextMode(); 00465 } 00466 00467 ImageWithNameList TextEdit::imagesWithName() const 00468 { 00469 ImageWithNameList retImages; 00470 QStringList seenImageNames; 00471 QList<QTextImageFormat> imageFormats = d->embeddedImageFormats(); 00472 foreach( const QTextImageFormat &imageFormat, imageFormats ) { 00473 if ( !seenImageNames.contains( imageFormat.name() ) ) { 00474 QVariant resourceData = document()->resource( QTextDocument::ImageResource, QUrl( imageFormat.name() ) ); 00475 QImage image = qvariant_cast<QImage>( resourceData ); 00476 QString name = imageFormat.name(); 00477 ImageWithNamePtr newImage( new ImageWithName ); 00478 newImage->image = image; 00479 newImage->name = name; 00480 retImages.append( newImage ); 00481 seenImageNames.append( imageFormat.name() ); 00482 } 00483 } 00484 return retImages; 00485 } 00486 00487 QList< QSharedPointer<EmbeddedImage> > TextEdit::embeddedImages() const 00488 { 00489 ImageWithNameList normalImages = imagesWithName(); 00490 QList< QSharedPointer<EmbeddedImage> > retImages; 00491 foreach( const ImageWithNamePtr &normalImage, normalImages ) { 00492 QBuffer buffer; 00493 buffer.open( QIODevice::WriteOnly ); 00494 normalImage->image.save( &buffer, "PNG" ); 00495 00496 qsrand( QDateTime::currentDateTime().toTime_t() + qHash( normalImage->name ) ); 00497 QSharedPointer<EmbeddedImage> embeddedImage( new EmbeddedImage() ); 00498 retImages.append( embeddedImage ); 00499 embeddedImage->image = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() ); 00500 embeddedImage->imageName = normalImage->name; 00501 embeddedImage->contentID = QString( QLatin1String( "%1@KDE" ) ).arg( qrand() ); 00502 } 00503 return retImages; 00504 } 00505 00506 QList<QTextImageFormat> TextEditPrivate::embeddedImageFormats() const 00507 { 00508 QTextDocument *doc = q->document(); 00509 QList<QTextImageFormat> retList; 00510 00511 QTextBlock currentBlock = doc->begin(); 00512 while ( currentBlock.isValid() ) { 00513 QTextBlock::iterator it; 00514 for ( it = currentBlock.begin(); !it.atEnd(); ++it ) { 00515 QTextFragment fragment = it.fragment(); 00516 if ( fragment.isValid() ) { 00517 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat(); 00518 if ( imageFormat.isValid() ) { 00519 retList.append( imageFormat ); 00520 } 00521 } 00522 } 00523 currentBlock = currentBlock.next(); 00524 } 00525 return retList; 00526 } 00527 00528 void TextEditPrivate::_k_slotAddImage() 00529 { 00530 QPointer<KFileDialog> fdlg = new KFileDialog( QString(), QString(), q ); 00531 fdlg->setOperationMode( KFileDialog::Other ); 00532 fdlg->setCaption( i18n("Add Image") ); 00533 fdlg->okButton()->setGuiItem( KGuiItem( i18n("&Add"), QLatin1String( "document-open" ) ) ); 00534 fdlg->setMode( KFile::Files ); 00535 if ( fdlg->exec() != KDialog::Accepted ) { 00536 delete fdlg; 00537 return; 00538 } 00539 00540 const KUrl::List files = fdlg->selectedUrls(); 00541 foreach ( const KUrl& url, files ) { 00542 q->addImage( url ); 00543 } 00544 delete fdlg; 00545 } 00546 00547 void KPIMTextEdit::TextEdit::enableImageActions() 00548 { 00549 d->imageSupportEnabled = true; 00550 } 00551 00552 bool KPIMTextEdit::TextEdit::isEnableImageActions() const 00553 { 00554 return d->imageSupportEnabled; 00555 } 00556 00557 QByteArray KPIMTextEdit::TextEdit::imageNamesToContentIds( const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList ) 00558 { 00559 QByteArray result = htmlBody; 00560 if ( imageList.size() > 0 ) { 00561 foreach( const QSharedPointer<EmbeddedImage> &image, imageList ) { 00562 const QString newImageName = QLatin1String( "cid:" ) + image->contentID; 00563 QByteArray quote( "\"" ); 00564 result.replace( QByteArray( quote + image->imageName.toLocal8Bit() + quote ), 00565 QByteArray( quote + newImageName.toLocal8Bit() + quote ) ); 00566 } 00567 } 00568 return result; 00569 } 00570 00571 void TextEdit::insertImage( const QImage &image, const QFileInfo&fileInfo ) 00572 { 00573 QString imageName = fileInfo.baseName().isEmpty() ? i18nc( "Start of the filename for an image", "image" ) : fileInfo.baseName(); 00574 d->addImageHelper( imageName, image ); 00575 } 00576 00577 void TextEdit::insertFromMimeData( const QMimeData *source ) 00578 { 00579 // Add an image if that is on the clipboard 00580 if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) { 00581 QImage image = qvariant_cast<QImage>( source->imageData() ); 00582 QFileInfo fi( source->text() ); 00583 insertImage( image, fi ); 00584 return; 00585 } 00586 00587 // Attempt to paste HTML contents into the text edit in plain text mode, 00588 // prevent this and prevent plain text instead. 00589 if ( textMode() == KRichTextEdit::Plain && source->hasHtml() ) { 00590 if ( source->hasText() ) { 00591 insertPlainText( source->text() ); 00592 return; 00593 } 00594 } 00595 00596 KRichTextWidget::insertFromMimeData( source ); 00597 } 00598 00599 bool KPIMTextEdit::TextEdit::canInsertFromMimeData( const QMimeData *source ) const 00600 { 00601 if ( source->hasHtml() && textMode() == KRichTextEdit::Rich ) 00602 return true; 00603 if ( source->hasText() ) 00604 return true; 00605 if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) 00606 return true; 00607 00608 return KRichTextWidget::canInsertFromMimeData( source ); 00609 } 00610 00611 bool TextEdit::isFormattingUsed() const 00612 { 00613 if ( textMode() == Plain ) 00614 return false; 00615 00616 return TextUtils::containsFormatting( document() ); 00617 } 00618 00619 void TextEditPrivate::_k_slotDeleteLine() 00620 { 00621 if ( q->hasFocus() ) 00622 q->deleteCurrentLine(); 00623 } 00624 00625 void TextEdit::deleteCurrentLine() 00626 { 00627 QTextCursor cursor = textCursor(); 00628 QTextBlock block = cursor.block(); 00629 const QTextLayout* layout = block.layout(); 00630 00631 // The current text block can have several lines due to word wrapping. 00632 // Search the line the cursor is in, and then delete it. 00633 for ( int lineNumber = 0; lineNumber < layout->lineCount(); lineNumber++ ) { 00634 QTextLine line = layout->lineAt( lineNumber ); 00635 const bool lastLineInBlock = ( line.textStart() + line.textLength() == block.length() - 1 ); 00636 const bool oneLineBlock = ( layout->lineCount() == 1 ); 00637 const int startOfLine = block.position() + line.textStart(); 00638 int endOfLine = block.position() + line.textStart() + line.textLength(); 00639 if ( !lastLineInBlock ) 00640 endOfLine -= 1; 00641 00642 // Found the line where the cursor is in 00643 if ( cursor.position() >= startOfLine && cursor.position() <= endOfLine ) { 00644 int deleteStart = startOfLine; 00645 int deleteLength = line.textLength(); 00646 if ( oneLineBlock ) 00647 deleteLength++; // The trailing newline 00648 00649 // When deleting the last line in the document, 00650 // remove the newline of the line before the last line instead 00651 if ( deleteStart + deleteLength >= document()->characterCount() && 00652 deleteStart > 0 ) 00653 deleteStart--; 00654 00655 cursor.beginEditBlock(); 00656 cursor.setPosition( deleteStart ); 00657 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength ); 00658 cursor.removeSelectedText(); 00659 cursor.endEditBlock(); 00660 return; 00661 } 00662 } 00663 00664 } 00665 00666 00667 #include "textedit.moc"