• Skip to content
  • Skip to link menu
KDE 4.3 API Reference
  • KDE API Reference
  • KDE-PIM Libraries
  • Sitemap
  • Contact Us
 

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 namespace KPIMTextEdit {
00046 
00047 class TextEditPrivate
00048 {
00049   public:
00050 
00051     TextEditPrivate( TextEdit *parent )
00052       : q( parent ),
00053         imageSupportEnabled( false )
00054     {
00055     }
00056 
00065     void addImageHelper( const QString &imageName, const QImage &image );
00066 
00070     QList<QTextImageFormat> embeddedImageFormats() const;
00071 
00076     void fixupTextEditString( QString &text ) const;
00077 
00081     void init();
00082 
00087     void _k_slotAddImage();
00088 
00090     KAction *actionAddImage;
00091 
00093     TextEdit *q;
00094 
00096     bool imageSupportEnabled;
00097 
00103     QStringList mImageNames;
00104 
00116     bool spellCheckingEnabled;
00117 };
00118 
00119 } // namespace
00120 
00121 using namespace KPIMTextEdit;
00122 
00123 void TextEditPrivate::fixupTextEditString( QString &text ) const
00124 {
00125   // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here
00126   text.remove( QChar::LineSeparator );
00127 
00128   // Get rid of embedded images, see QTextImageFormat documentation:
00129   // "Inline images are represented by an object replacement character (0xFFFC in Unicode) "
00130   text.remove( 0xFFFC );
00131 
00132   // In plaintext mode, each space is non-breaking.
00133   text.replace( QChar::Nbsp, QChar::fromAscii( ' ' ) );
00134 }
00135 
00136 TextEdit::TextEdit( const QString& text, QWidget *parent )
00137   : KRichTextWidget( text, parent ),
00138     d( new TextEditPrivate( this ) )
00139 {
00140   d->init();
00141 }
00142 
00143 TextEdit::TextEdit( QWidget *parent )
00144   : KRichTextWidget( parent ),
00145     d( new TextEditPrivate( this ) )
00146 {
00147   d->init();
00148 }
00149 
00150 TextEdit::~TextEdit()
00151 {
00152 }
00153 
00154 bool TextEdit::eventFilter( QObject*o, QEvent* e )
00155 {
00156   if ( o == this )
00157     KCursor::autoHideEventFilter( o, e );
00158   return KRichTextWidget::eventFilter( o, e );
00159 }
00160 
00161 void TextEditPrivate::init()
00162 {
00163   q->setSpellInterface( q );
00164   // We tell the KRichTextWidget to enable spell checking, because only then it will
00165   // call createHighlighter() which will create our own highlighter which also
00166   // does quote highlighting.
00167   // However, *our* spellchecking is still disabled. Our own highlighter only
00168   // cares about our spellcheck status, it will not highlight missspelled words
00169   // if our spellchecking is disabled.
00170   // See also KEMailQuotingHighlighter::highlightBlock().
00171   spellCheckingEnabled = false;
00172   q->setCheckSpellingEnabledInternal( true );
00173 
00174   KCursor::setAutoHideCursor( q, true, true );
00175   q->installEventFilter( q );
00176 }
00177 
00178 void TextEdit::keyPressEvent ( QKeyEvent * e )
00179 {
00180   if ( e->key() ==  Qt::Key_Return ) {
00181     QTextCursor cursor = textCursor();
00182     int oldPos = cursor.position();
00183     int blockPos = cursor.block().position();
00184 
00185     //selection all the line.
00186     cursor.movePosition( QTextCursor::StartOfBlock );
00187     cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
00188     QString lineText = cursor.selectedText();
00189     if ( ( ( oldPos -blockPos )  > 0 ) &&
00190          ( ( oldPos-blockPos ) < int( lineText.length() ) ) ) {
00191       bool isQuotedLine = false;
00192       int bot = 0; // bot = begin of text after quote indicators
00193       while ( bot < lineText.length() ) {
00194         if( ( lineText[bot] == QChar::fromAscii( '>' ) ) ||
00195             ( lineText[bot] == QChar::fromAscii( '|' ) ) ) {
00196           isQuotedLine = true;
00197           ++bot;
00198         }
00199         else if ( lineText[bot].isSpace() ) {
00200           ++bot;
00201         }
00202         else {
00203           break;
00204         }
00205       }
00206       KRichTextWidget::keyPressEvent( e );
00207       // duplicate quote indicators of the previous line before the new
00208       // line if the line actually contained text (apart from the quote
00209       // indicators) and the cursor is behind the quote indicators
00210       if ( isQuotedLine
00211            && ( bot != lineText.length() )
00212            && ( ( oldPos-blockPos ) >= int( bot ) ) ) {
00213         // The cursor position might have changed unpredictably if there was selected
00214         // text which got replaced by a new line, so we query it again:
00215         cursor.movePosition( QTextCursor::StartOfBlock );
00216         cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
00217         QString newLine = cursor.selectedText();
00218 
00219         // remove leading white space from the new line and instead
00220         // add the quote indicators of the previous line
00221         int leadingWhiteSpaceCount = 0;
00222         while ( ( leadingWhiteSpaceCount < newLine.length() )
00223                   && newLine[leadingWhiteSpaceCount].isSpace() ) {
00224           ++leadingWhiteSpaceCount;
00225         }
00226         newLine = newLine.replace( 0, leadingWhiteSpaceCount,
00227                                    lineText.left( bot ) );
00228         cursor.insertText( newLine );
00229         //cursor.setPosition( cursor.position() + 2);
00230         cursor.movePosition( QTextCursor::StartOfBlock );
00231         setTextCursor( cursor );
00232       }
00233     }
00234     else
00235       KRichTextWidget::keyPressEvent( e );
00236   }
00237   else
00238   {
00239     KRichTextWidget::keyPressEvent( e );
00240   }
00241 }
00242 
00243 
00244 bool TextEdit::isSpellCheckingEnabled() const
00245 {
00246   return d->spellCheckingEnabled;
00247 }
00248 
00249 void TextEdit::setSpellCheckingEnabled( bool enable )
00250 {
00251   EMailQuoteHighlighter *hlighter =
00252       dynamic_cast<EMailQuoteHighlighter*>( highlighter() );
00253   if ( hlighter )
00254     hlighter->toggleSpellHighlighting( enable );
00255 
00256   d->spellCheckingEnabled = enable;
00257   emit checkSpellingChanged( enable );
00258 }
00259 
00260 bool TextEdit::shouldBlockBeSpellChecked( const QString& block ) const
00261 {
00262   return !isLineQuoted( block );
00263 }
00264 
00265 bool KPIMTextEdit::TextEdit::isLineQuoted( const QString& line ) const
00266 {
00267   return quoteLength( line ) > 0;
00268 }
00269 
00270 int KPIMTextEdit::TextEdit::quoteLength( const QString& line ) const
00271 {
00272   bool quoteFound = false;
00273   int startOfText = -1;
00274   for ( int i = 0; i < line.length(); i++ ) {
00275     if ( line[i] == QLatin1Char( '>' ) || line[i] == QLatin1Char( '|' ) )
00276       quoteFound = true;
00277     else if ( line[i] != QLatin1Char( ' ' ) ) {
00278       startOfText = i;
00279       break;
00280     }
00281   }
00282   if ( quoteFound ) {
00283     if ( startOfText == -1 )
00284       startOfText = line.length() - 1;
00285     return startOfText;
00286   }
00287   else
00288     return 0;
00289 }
00290 
00291 const QString KPIMTextEdit::TextEdit::defaultQuoteSign() const
00292 {
00293   return QLatin1String( "> " );
00294 }
00295 
00296 void TextEdit::createHighlighter()
00297 {
00298   EMailQuoteHighlighter *emailHighLighter =
00299       new EMailQuoteHighlighter( this );
00300 
00301   setHighlighterColors( emailHighLighter );
00302 
00303   //TODO change config
00304   KRichTextWidget::setHighlighter( emailHighLighter );
00305 
00306   if ( !spellCheckingLanguage().isEmpty() )
00307     setSpellCheckingLanguage( spellCheckingLanguage() );
00308   setSpellCheckingEnabled( isSpellCheckingEnabled() );
00309 }
00310 
00311 void TextEdit::setHighlighterColors( EMailQuoteHighlighter *highlighter )
00312 {
00313   Q_UNUSED( highlighter );
00314 }
00315 
00316 QString TextEdit::toWrappedPlainText() const
00317 {
00318   QString temp;
00319   QTextDocument* doc = document();
00320   QTextBlock block = doc->begin();
00321   while ( block.isValid() ) {
00322     QTextLayout* layout = block.layout();
00323     for ( int i = 0; i < layout->lineCount(); i++ ) {
00324       QTextLine line = layout->lineAt( i );
00325       temp += block.text().mid( line.textStart(), line.textLength() ) + QLatin1Char( '\n' );
00326     }
00327     block = block.next();
00328   }
00329 
00330   // Remove the last superfluous newline added above
00331   if ( temp.endsWith( QLatin1Char( '\n' ) ) )
00332     temp.chop( 1 );
00333 
00334   d->fixupTextEditString( temp );
00335   return temp;
00336 }
00337 
00338 QString TextEdit::toCleanPlainText() const
00339 {
00340   QString temp = toPlainText();
00341   d->fixupTextEditString( temp );
00342   return temp;
00343 }
00344 
00345 void TextEdit::createActions( KActionCollection *actionCollection )
00346 {
00347   KRichTextWidget::createActions( actionCollection );
00348 
00349   if ( d->imageSupportEnabled ) {
00350     d->actionAddImage = new KAction( KIcon( QLatin1String( "insert-image" ) ),
00351                                     i18n( "Add Image" ), this );
00352     actionCollection->addAction( QLatin1String( "add_image" ), d->actionAddImage );
00353     connect( d->actionAddImage, SIGNAL(triggered(bool) ), SLOT( _k_slotAddImage() ) );
00354   }
00355 }
00356 
00357 void TextEdit::addImage( const KUrl &url )
00358 {
00359   QImage image;
00360   if ( !image.load( url.path() ) ) {
00361     KMessageBox::error( this,
00362                         i18nc( "@info", "Unable to load image <filename>%1</filename>.", url.path() ) );
00363     return;
00364   }
00365   QFileInfo fi( url.path() );
00366   QString imageName = fi.baseName().isEmpty() ? QLatin1String( "image.png" )
00367                                               : fi.baseName() + QLatin1String( ".png" );
00368   d->addImageHelper( imageName, image );
00369 }
00370 
00371 void TextEditPrivate::addImageHelper( const QString &imageName, const QImage &image )
00372 {
00373   QString imageNameToAdd = imageName;
00374   QTextDocument *document = q->document();
00375 
00376   // determine the imageNameToAdd
00377   int imageNumber = 1;
00378   while ( mImageNames.contains( imageNameToAdd ) ) {
00379     QVariant qv = document->resource( QTextDocument::ImageResource, QUrl( imageNameToAdd ) );
00380     if ( qv == image ) {
00381       // use the same name
00382       break;
00383     }
00384     int firstDot = imageName.indexOf( QLatin1Char( '.' ) );
00385     if ( firstDot == -1 )
00386       imageNameToAdd = imageName + QString::number( imageNumber++ );
00387     else
00388       imageNameToAdd = imageName.left( firstDot ) + QString::number( imageNumber++ ) +
00389                        imageName.mid( firstDot );
00390   }
00391 
00392   if ( !mImageNames.contains( imageNameToAdd ) ) {
00393     document->addResource( QTextDocument::ImageResource, QUrl( imageNameToAdd ), image );
00394     mImageNames << imageNameToAdd;
00395   }
00396   q->textCursor().insertImage( imageNameToAdd );
00397   q->enableRichTextMode();
00398 }
00399 
00400 QList< QSharedPointer<EmbeddedImage> > TextEdit::embeddedImages() const
00401 {
00402   QList< QSharedPointer<EmbeddedImage> > retImages;
00403   QStringList seenImageNames;
00404   QList<QTextImageFormat> imageFormats = d->embeddedImageFormats();
00405   foreach( const QTextImageFormat &imageFormat, imageFormats ) {
00406     if ( !seenImageNames.contains( imageFormat.name() ) ) {
00407       QVariant data = document()->resource( QTextDocument::ImageResource, QUrl( imageFormat.name() ) );
00408       QImage image = qvariant_cast<QImage>( data );
00409       QBuffer buffer;
00410       buffer.open( QIODevice::WriteOnly );
00411       image.save( &buffer, "PNG" );
00412 
00413       qsrand( QDateTime::currentDateTime().toTime_t() + qHash( imageFormat.name() ) );
00414       QSharedPointer<EmbeddedImage> embeddedImage( new EmbeddedImage() );
00415       retImages.append( embeddedImage );
00416       embeddedImage->image = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() );
00417       embeddedImage->imageName = imageFormat.name();
00418       embeddedImage->contentID = QString( QLatin1String( "%1" ) ).arg( qrand() );
00419       seenImageNames.append( imageFormat.name() );
00420     }
00421   }
00422   return retImages;
00423 }
00424 
00425 QList<QTextImageFormat> TextEditPrivate::embeddedImageFormats() const
00426 {
00427   QTextDocument *doc = q->document();
00428   QList<QTextImageFormat> retList;
00429 
00430   QTextBlock currentBlock = doc->begin();
00431   while ( currentBlock.isValid() ) {
00432     QTextBlock::iterator it;
00433     for ( it = currentBlock.begin(); !it.atEnd(); ++it ) {
00434       QTextFragment fragment = it.fragment();
00435       if ( fragment.isValid() ) {
00436         QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
00437         if ( imageFormat.isValid() ) {
00438           retList.append( imageFormat );
00439         }
00440       }
00441     }
00442     currentBlock = currentBlock.next();
00443   }
00444   return retList;
00445 }
00446 
00447 void TextEditPrivate::_k_slotAddImage()
00448 {
00449   QPointer<KFileDialog> fdlg = new KFileDialog( QString(), QString(), q );
00450   fdlg->setOperationMode( KFileDialog::Other );
00451   fdlg->setCaption( i18n("Add Image") );
00452   fdlg->okButton()->setGuiItem( KGuiItem( i18n("&Add"), QLatin1String( "document-open" ) ) );
00453   fdlg->setMode( KFile::Files );
00454   if ( fdlg->exec() != KDialog::Accepted ) {
00455     delete fdlg;
00456     return;
00457   }
00458 
00459   const KUrl::List files = fdlg->selectedUrls();
00460   foreach ( const KUrl& url, files ) {
00461     q->addImage( url );
00462   }
00463   delete fdlg;
00464 }
00465 
00466 void KPIMTextEdit::TextEdit::enableImageActions()
00467 {
00468   d->imageSupportEnabled = true;
00469 }
00470 
00471 QByteArray KPIMTextEdit::TextEdit::imageNamesToContentIds( const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList )
00472 {
00473   QByteArray result = htmlBody;
00474   if ( imageList.size() > 0 ) {
00475     foreach( const QSharedPointer<EmbeddedImage> &image, imageList ) {
00476       const QString newImageName = QLatin1String( "cid:" ) + image->contentID;
00477       QByteArray quote( "\"" );
00478       result.replace( QByteArray( quote + image->imageName.toLocal8Bit() + quote ),
00479                       QByteArray( quote + newImageName.toLocal8Bit() + quote ) );
00480     }
00481   }
00482   return result;
00483 }
00484 
00485 void TextEdit::insertFromMimeData( const QMimeData *source )
00486 {
00487   // Add an image if that is on the clipboard
00488   if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) {
00489     QImage image = qvariant_cast<QImage>( source->imageData() );
00490     QFileInfo fi( source->text() );
00491     QString imageName = fi.baseName().isEmpty() ? i18nc( "Start of the filename for an image", "image" ) : fi.baseName();
00492     d->addImageHelper( imageName, image );
00493     return;
00494   }
00495 
00496   // Attempt to paste HTML contents into the text edit in plain text mode,
00497   // prevent this and prevent plain text instead.
00498   if ( textMode() == KRichTextEdit::Plain && source->hasHtml() ) {
00499     if ( source->hasText() ) {
00500       insertPlainText( source->text() );
00501       return;
00502     }
00503   }
00504 
00505   KRichTextWidget::insertFromMimeData( source );
00506 }
00507 
00508 bool KPIMTextEdit::TextEdit::canInsertFromMimeData( const QMimeData *source ) const
00509 {
00510   if ( source->hasHtml() && textMode() == KRichTextEdit::Rich )
00511     return true;
00512   if ( source->hasText() )
00513     return true;
00514   if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled )
00515     return true;
00516 
00517   return KRichTextWidget::canInsertFromMimeData( source );
00518 }
00519 
00520 static bool isCharFormatFormatted( const QTextCharFormat &format, const QFont &defaultFont,
00521                                    const QTextCharFormat &defaultBlockFormat )
00522 {
00523   if ( !format.anchorHref().isEmpty() ||
00524        format.font() != defaultFont ||
00525        format.isAnchor() ||
00526        format.verticalAlignment() != defaultBlockFormat.verticalAlignment() ||
00527        format.underlineStyle() != defaultBlockFormat.underlineStyle() ||
00528        format.foreground().color() != defaultBlockFormat.foreground().color() ||
00529        format.background().color() != defaultBlockFormat.background().color() )
00530     return true;
00531 
00532   return false;
00533 }
00534 
00535 static bool isBlockFormatFormatted( const QTextBlockFormat &format,
00536                                     const QTextBlockFormat &defaultFormat )
00537 {
00538   if ( format.alignment() != defaultFormat.alignment() ||
00539        format.indent() != defaultFormat.indent() ||
00540        format.textIndent() != defaultFormat.textIndent() )
00541     return true;
00542 
00543   return false;
00544 }
00545 
00547 static bool isSpecial( const QTextFormat &charFormat )
00548 {
00549   return charFormat.isFrameFormat() || charFormat.isImageFormat() ||
00550          charFormat.isListFormat() || charFormat.isTableFormat();
00551 }
00552 
00553 bool TextEdit::isFormattingUsed() const
00554 {
00555   if ( textMode() == Plain )
00556     return false;
00557 
00558   // Below, we walk through all text blocks and through all text fragments in them
00559   // and check if any of those has any formatting.
00560   // To check if they have formatting, we use the functions isBlockFormatFormatted() and
00561   // isCharFormatFormatted(). Those do not check all the exising formatting possibilities on
00562   // earth, but everything that KRichTextEdit supports at the moment.
00563   //
00564   // Also, we have to compare the formats against those of a default text edit. For example,
00565   // we can't compare the foreground color against black, because the user might have another
00566   // color scheme. Therefore we compare the foreground color against a default text edit.
00567 
00568   QTextEdit defaultTextEdit;
00569   QTextCharFormat defaultCharFormat = defaultTextEdit.document()->begin().charFormat();
00570   QTextBlockFormat defaultBlockFormat = defaultTextEdit.document()->begin().blockFormat();
00571   QFont defaultFont = document()->defaultFont();
00572 
00573   QTextBlock block = document()->firstBlock();
00574   while ( block.isValid() ) {
00575 
00576     if ( isBlockFormatFormatted( block.blockFormat(), defaultBlockFormat ) ) {
00577       return true;
00578     }
00579 
00580     if ( isSpecial( block.charFormat() ) || isSpecial( block.blockFormat() ) ||
00581          block.textList() ) {
00582       return true;
00583     }
00584 
00585     QTextBlock::iterator it = block.begin();
00586     while ( !it.atEnd() ) {
00587       QTextFragment fragment = it.fragment();
00588       QTextCharFormat charFormat = fragment.charFormat();
00589       if ( isSpecial( charFormat ) ) {
00590         return true;
00591       }
00592       if ( isCharFormatFormatted( fragment.charFormat(), defaultFont, defaultCharFormat ) ) {
00593         return true;
00594       }
00595 
00596       it++;
00597     }
00598     block = block.next();
00599   }
00600 
00601   if ( toHtml().contains( QLatin1String( "<hr />" ) ) )
00602     return true;
00603 
00604   return false;
00605 }
00606 
00607 #include "textedit.moc"

KPIMTextedit Library

Skip menu "KPIMTextedit Library"
  • Main Page
  • Namespace List
  • Alphabetical List
  • Class List
  • File List
  • Class Members
  • Related Pages

KDE-PIM Libraries

Skip menu "KDE-PIM Libraries"
  • akonadi
  • kabc
  • kblog
  • kcal
  • kholidays
  • kimap
  • kioslave
  •   imap4
  •   mbox
  • kldap
  • kmime
  • kpimidentities
  • kpimtextedit
  •   richtextbuilders
  • kpimutils
  • kresources
  • ktnef
  • kxmlrpcclient
  • mailtransport
  • microblog
  • qgpgme
  • syndication
  •   atom
  •   rdf
  •   rss2
Generated for KDE-PIM Libraries by doxygen 1.6.1
This website is maintained by Adriaan de Groot and Allen Winter.
KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal