00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
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 : actionAddImage( 0 ),
00053 actionDeleteLine( 0 ),
00054 q( parent ),
00055 imageSupportEnabled( false )
00056 {
00057 }
00058
00067 void addImageHelper( const QString &imageName, const QImage &image );
00068
00072 QList<QTextImageFormat> embeddedImageFormats() const;
00073
00078 void fixupTextEditString( QString &text ) const;
00079
00083 void init();
00084
00089 void _k_slotAddImage();
00090
00091 void _k_slotDeleteLine();
00092
00094 KAction *actionAddImage;
00095
00097 KAction *actionDeleteLine;
00098
00100 TextEdit *q;
00101
00103 bool imageSupportEnabled;
00104
00110 QStringList mImageNames;
00111
00123 bool spellCheckingEnabled;
00124 };
00125
00126 }
00127
00128 using namespace KPIMTextEdit;
00129
00130 void TextEditPrivate::fixupTextEditString( QString &text ) const
00131 {
00132
00133 text.remove( QChar::LineSeparator );
00134
00135
00136
00137 text.remove( 0xFFFC );
00138
00139
00140 text.replace( QChar::Nbsp, QChar::fromAscii( ' ' ) );
00141 }
00142
00143 TextEdit::TextEdit( const QString& text, QWidget *parent )
00144 : KRichTextWidget( text, parent ),
00145 d( new TextEditPrivate( this ) )
00146 {
00147 d->init();
00148 }
00149
00150 TextEdit::TextEdit( QWidget *parent )
00151 : KRichTextWidget( parent ),
00152 d( new TextEditPrivate( this ) )
00153 {
00154 d->init();
00155 }
00156
00157 TextEdit::~TextEdit()
00158 {
00159 }
00160
00161 bool TextEdit::eventFilter( QObject*o, QEvent* e )
00162 {
00163 if ( o == this )
00164 KCursor::autoHideEventFilter( o, e );
00165 return KRichTextWidget::eventFilter( o, e );
00166 }
00167
00168 void TextEditPrivate::init()
00169 {
00170 q->setSpellInterface( q );
00171
00172
00173
00174
00175
00176
00177
00178 spellCheckingEnabled = false;
00179 q->setCheckSpellingEnabledInternal( true );
00180
00181 KCursor::setAutoHideCursor( q, true, true );
00182 q->installEventFilter( q );
00183 }
00184
00185 void TextEdit::keyPressEvent ( QKeyEvent * e )
00186 {
00187 if ( e->key() == Qt::Key_Return ) {
00188 QTextCursor cursor = textCursor();
00189 int oldPos = cursor.position();
00190 int blockPos = cursor.block().position();
00191
00192
00193 cursor.movePosition( QTextCursor::StartOfBlock );
00194 cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
00195 QString lineText = cursor.selectedText();
00196 if ( ( ( oldPos -blockPos ) > 0 ) &&
00197 ( ( oldPos-blockPos ) < int( lineText.length() ) ) ) {
00198 bool isQuotedLine = false;
00199 int bot = 0;
00200 while ( bot < lineText.length() ) {
00201 if( ( lineText[bot] == QChar::fromAscii( '>' ) ) ||
00202 ( lineText[bot] == QChar::fromAscii( '|' ) ) ) {
00203 isQuotedLine = true;
00204 ++bot;
00205 }
00206 else if ( lineText[bot].isSpace() ) {
00207 ++bot;
00208 }
00209 else {
00210 break;
00211 }
00212 }
00213 KRichTextWidget::keyPressEvent( e );
00214
00215
00216
00217 if ( isQuotedLine
00218 && ( bot != lineText.length() )
00219 && ( ( oldPos-blockPos ) >= int( bot ) ) ) {
00220
00221
00222 cursor.movePosition( QTextCursor::StartOfBlock );
00223 cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
00224 QString newLine = cursor.selectedText();
00225
00226
00227
00228 int leadingWhiteSpaceCount = 0;
00229 while ( ( leadingWhiteSpaceCount < newLine.length() )
00230 && newLine[leadingWhiteSpaceCount].isSpace() ) {
00231 ++leadingWhiteSpaceCount;
00232 }
00233 newLine = newLine.replace( 0, leadingWhiteSpaceCount,
00234 lineText.left( bot ) );
00235 cursor.insertText( newLine );
00236
00237 cursor.movePosition( QTextCursor::StartOfBlock );
00238 setTextCursor( cursor );
00239 }
00240 }
00241 else
00242 KRichTextWidget::keyPressEvent( e );
00243 }
00244 else
00245 {
00246 KRichTextWidget::keyPressEvent( e );
00247 }
00248 }
00249
00250
00251 bool TextEdit::isSpellCheckingEnabled() const
00252 {
00253 return d->spellCheckingEnabled;
00254 }
00255
00256 void TextEdit::setSpellCheckingEnabled( bool enable )
00257 {
00258 EMailQuoteHighlighter *hlighter =
00259 dynamic_cast<EMailQuoteHighlighter*>( highlighter() );
00260 if ( hlighter )
00261 hlighter->toggleSpellHighlighting( enable );
00262
00263 d->spellCheckingEnabled = enable;
00264 emit checkSpellingChanged( enable );
00265 }
00266
00267 bool TextEdit::shouldBlockBeSpellChecked( const QString& block ) const
00268 {
00269 return !isLineQuoted( block );
00270 }
00271
00272 bool KPIMTextEdit::TextEdit::isLineQuoted( const QString& line ) const
00273 {
00274 return quoteLength( line ) > 0;
00275 }
00276
00277 int KPIMTextEdit::TextEdit::quoteLength( const QString& line ) const
00278 {
00279 bool quoteFound = false;
00280 int startOfText = -1;
00281 for ( int i = 0; i < line.length(); i++ ) {
00282 if ( line[i] == QLatin1Char( '>' ) || line[i] == QLatin1Char( '|' ) )
00283 quoteFound = true;
00284 else if ( line[i] != QLatin1Char( ' ' ) ) {
00285 startOfText = i;
00286 break;
00287 }
00288 }
00289 if ( quoteFound ) {
00290 if ( startOfText == -1 )
00291 startOfText = line.length() - 1;
00292 return startOfText;
00293 }
00294 else
00295 return 0;
00296 }
00297
00298 const QString KPIMTextEdit::TextEdit::defaultQuoteSign() const
00299 {
00300 return QLatin1String( "> " );
00301 }
00302
00303 void TextEdit::createHighlighter()
00304 {
00305 EMailQuoteHighlighter *emailHighLighter =
00306 new EMailQuoteHighlighter( this );
00307
00308 setHighlighterColors( emailHighLighter );
00309
00310
00311 KRichTextWidget::setHighlighter( emailHighLighter );
00312
00313 if ( !spellCheckingLanguage().isEmpty() )
00314 setSpellCheckingLanguage( spellCheckingLanguage() );
00315 setSpellCheckingEnabled( isSpellCheckingEnabled() );
00316 }
00317
00318 void TextEdit::setHighlighterColors( EMailQuoteHighlighter *highlighter )
00319 {
00320 Q_UNUSED( highlighter );
00321 }
00322
00323 QString TextEdit::toWrappedPlainText() const
00324 {
00325 QString temp;
00326 QTextDocument* doc = document();
00327 QTextBlock block = doc->begin();
00328 while ( block.isValid() ) {
00329 QTextLayout* layout = block.layout();
00330 for ( int i = 0; i < layout->lineCount(); i++ ) {
00331 QTextLine line = layout->lineAt( i );
00332 temp += block.text().mid( line.textStart(), line.textLength() ) + QLatin1Char( '\n' );
00333 }
00334 block = block.next();
00335 }
00336
00337
00338 if ( temp.endsWith( QLatin1Char( '\n' ) ) )
00339 temp.chop( 1 );
00340
00341 d->fixupTextEditString( temp );
00342 return temp;
00343 }
00344
00345 QString TextEdit::toCleanPlainText() const
00346 {
00347 QString temp = toPlainText();
00348 d->fixupTextEditString( temp );
00349 return temp;
00350 }
00351
00352 void TextEdit::createActions( KActionCollection *actionCollection )
00353 {
00354 KRichTextWidget::createActions( actionCollection );
00355
00356 if ( d->imageSupportEnabled ) {
00357 d->actionAddImage = new KAction( KIcon( QLatin1String( "insert-image" ) ),
00358 i18n( "Add Image" ), this );
00359 actionCollection->addAction( QLatin1String( "add_image" ), d->actionAddImage );
00360 connect( d->actionAddImage, SIGNAL(triggered(bool) ), SLOT( _k_slotAddImage() ) );
00361 }
00362
00363 d->actionDeleteLine = new KAction( i18n( "Delete Line" ), this );
00364 d->actionDeleteLine->setShortcut( QKeySequence( Qt::CTRL + Qt::Key_K ) );
00365 actionCollection->addAction( QLatin1String( "delete_line" ), d->actionDeleteLine );
00366 connect( d->actionDeleteLine, SIGNAL(triggered(bool)), SLOT(_k_slotDeleteLine()) );
00367 }
00368
00369 void TextEdit::addImage( const KUrl &url )
00370 {
00371 QImage image;
00372 if ( !image.load( url.path() ) ) {
00373 KMessageBox::error( this,
00374 i18nc( "@info", "Unable to load image <filename>%1</filename>.", url.path() ) );
00375 return;
00376 }
00377 QFileInfo fi( url.path() );
00378 QString imageName = fi.baseName().isEmpty() ? QLatin1String( "image.png" )
00379 : fi.baseName() + QLatin1String( ".png" );
00380 d->addImageHelper( imageName, image );
00381 }
00382
00383 void TextEdit::loadImage ( const QImage& image, const QString& matchName, const QString& resourceName )
00384 {
00385 QSet<int> cursorPositionsToSkip;
00386 QTextBlock currentBlock = document()->begin();
00387 QTextBlock::iterator it;
00388 while ( currentBlock.isValid() ) {
00389 for (it = currentBlock.begin(); !(it.atEnd()); ++it) {
00390 QTextFragment fragment = it.fragment();
00391 if ( fragment.isValid() ) {
00392 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
00393 if ( imageFormat.isValid() && imageFormat.name() == matchName ) {
00394 int pos = fragment.position();
00395 if ( !cursorPositionsToSkip.contains( pos ) ) {
00396 QTextCursor cursor( document() );
00397 cursor.setPosition( pos );
00398 cursor.setPosition( pos + 1, QTextCursor::KeepAnchor );
00399 cursor.removeSelectedText();
00400 document()->addResource( QTextDocument::ImageResource, QUrl( resourceName ), QVariant( image ) );
00401 cursor.insertImage( resourceName );
00402
00403
00404
00405 cursorPositionsToSkip.insert( pos );
00406 it = currentBlock.begin();
00407 }
00408 }
00409 }
00410 }
00411 currentBlock = currentBlock.next();
00412 }
00413 }
00414
00415 void TextEditPrivate::addImageHelper( const QString &imageName, const QImage &image )
00416 {
00417 QString imageNameToAdd = imageName;
00418 QTextDocument *document = q->document();
00419
00420
00421 int imageNumber = 1;
00422 while ( mImageNames.contains( imageNameToAdd ) ) {
00423 QVariant qv = document->resource( QTextDocument::ImageResource, QUrl( imageNameToAdd ) );
00424 if ( qv == image ) {
00425
00426 break;
00427 }
00428 int firstDot = imageName.indexOf( QLatin1Char( '.' ) );
00429 if ( firstDot == -1 )
00430 imageNameToAdd = imageName + QString::number( imageNumber++ );
00431 else
00432 imageNameToAdd = imageName.left( firstDot ) + QString::number( imageNumber++ ) +
00433 imageName.mid( firstDot );
00434 }
00435
00436 if ( !mImageNames.contains( imageNameToAdd ) ) {
00437 document->addResource( QTextDocument::ImageResource, QUrl( imageNameToAdd ), image );
00438 mImageNames << imageNameToAdd;
00439 }
00440 q->textCursor().insertImage( imageNameToAdd );
00441 q->enableRichTextMode();
00442 }
00443
00444 ImageWithNameList TextEdit::imagesWithName() const
00445 {
00446 ImageWithNameList retImages;
00447 QStringList seenImageNames;
00448 QList<QTextImageFormat> imageFormats = d->embeddedImageFormats();
00449 foreach( const QTextImageFormat &imageFormat, imageFormats ) {
00450 if ( !seenImageNames.contains( imageFormat.name() ) ) {
00451 QVariant data = document()->resource( QTextDocument::ImageResource, QUrl( imageFormat.name() ) );
00452 QImage image = qvariant_cast<QImage>( data );
00453 QString name = imageFormat.name();
00454 ImageWithNamePtr newImage( new ImageWithName );
00455 newImage->image = image;
00456 newImage->name = name;
00457 retImages.append( newImage );
00458 seenImageNames.append( imageFormat.name() );
00459 }
00460 }
00461 return retImages;
00462 }
00463
00464 QList< QSharedPointer<EmbeddedImage> > TextEdit::embeddedImages() const
00465 {
00466 ImageWithNameList normalImages = imagesWithName();
00467 QList< QSharedPointer<EmbeddedImage> > retImages;
00468 foreach( const ImageWithNamePtr &normalImage, normalImages ) {
00469 QBuffer buffer;
00470 buffer.open( QIODevice::WriteOnly );
00471 normalImage->image.save( &buffer, "PNG" );
00472
00473 qsrand( QDateTime::currentDateTime().toTime_t() + qHash( normalImage->name ) );
00474 QSharedPointer<EmbeddedImage> embeddedImage( new EmbeddedImage() );
00475 retImages.append( embeddedImage );
00476 embeddedImage->image = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() );
00477 embeddedImage->imageName = normalImage->name;
00478 embeddedImage->contentID = QString( QLatin1String( "%1@KDE" ) ).arg( qrand() );
00479 }
00480 return retImages;
00481 }
00482
00483 QList<QTextImageFormat> TextEditPrivate::embeddedImageFormats() const
00484 {
00485 QTextDocument *doc = q->document();
00486 QList<QTextImageFormat> retList;
00487
00488 QTextBlock currentBlock = doc->begin();
00489 while ( currentBlock.isValid() ) {
00490 QTextBlock::iterator it;
00491 for ( it = currentBlock.begin(); !it.atEnd(); ++it ) {
00492 QTextFragment fragment = it.fragment();
00493 if ( fragment.isValid() ) {
00494 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
00495 if ( imageFormat.isValid() ) {
00496 retList.append( imageFormat );
00497 }
00498 }
00499 }
00500 currentBlock = currentBlock.next();
00501 }
00502 return retList;
00503 }
00504
00505 void TextEditPrivate::_k_slotAddImage()
00506 {
00507 QPointer<KFileDialog> fdlg = new KFileDialog( QString(), QString(), q );
00508 fdlg->setOperationMode( KFileDialog::Other );
00509 fdlg->setCaption( i18n("Add Image") );
00510 fdlg->okButton()->setGuiItem( KGuiItem( i18n("&Add"), QLatin1String( "document-open" ) ) );
00511 fdlg->setMode( KFile::Files );
00512 if ( fdlg->exec() != KDialog::Accepted ) {
00513 delete fdlg;
00514 return;
00515 }
00516
00517 const KUrl::List files = fdlg->selectedUrls();
00518 foreach ( const KUrl& url, files ) {
00519 q->addImage( url );
00520 }
00521 delete fdlg;
00522 }
00523
00524 void KPIMTextEdit::TextEdit::enableImageActions()
00525 {
00526 d->imageSupportEnabled = true;
00527 }
00528
00529 QByteArray KPIMTextEdit::TextEdit::imageNamesToContentIds( const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList )
00530 {
00531 QByteArray result = htmlBody;
00532 if ( imageList.size() > 0 ) {
00533 foreach( const QSharedPointer<EmbeddedImage> &image, imageList ) {
00534 const QString newImageName = QLatin1String( "cid:" ) + image->contentID;
00535 QByteArray quote( "\"" );
00536 result.replace( QByteArray( quote + image->imageName.toLocal8Bit() + quote ),
00537 QByteArray( quote + newImageName.toLocal8Bit() + quote ) );
00538 }
00539 }
00540 return result;
00541 }
00542
00543 void TextEdit::insertFromMimeData( const QMimeData *source )
00544 {
00545
00546 if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) {
00547 QImage image = qvariant_cast<QImage>( source->imageData() );
00548 QFileInfo fi( source->text() );
00549 QString imageName = fi.baseName().isEmpty() ? i18nc( "Start of the filename for an image", "image" ) : fi.baseName();
00550 d->addImageHelper( imageName, image );
00551 return;
00552 }
00553
00554
00555
00556 if ( textMode() == KRichTextEdit::Plain && source->hasHtml() ) {
00557 if ( source->hasText() ) {
00558 insertPlainText( source->text() );
00559 return;
00560 }
00561 }
00562
00563 KRichTextWidget::insertFromMimeData( source );
00564 }
00565
00566 bool KPIMTextEdit::TextEdit::canInsertFromMimeData( const QMimeData *source ) const
00567 {
00568 if ( source->hasHtml() && textMode() == KRichTextEdit::Rich )
00569 return true;
00570 if ( source->hasText() )
00571 return true;
00572 if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled )
00573 return true;
00574
00575 return KRichTextWidget::canInsertFromMimeData( source );
00576 }
00577
00578 static bool isCharFormatFormatted( const QTextCharFormat &format, const QFont &defaultFont,
00579 const QTextCharFormat &defaultBlockFormat )
00580 {
00581 if ( !format.anchorHref().isEmpty() ||
00582 format.font() != defaultFont ||
00583 format.isAnchor() ||
00584 format.verticalAlignment() != defaultBlockFormat.verticalAlignment() ||
00585 format.underlineStyle() != defaultBlockFormat.underlineStyle() ||
00586 format.foreground().color() != defaultBlockFormat.foreground().color() ||
00587 format.background().color() != defaultBlockFormat.background().color() )
00588 return true;
00589
00590 return false;
00591 }
00592
00593 static bool isBlockFormatFormatted( const QTextBlockFormat &format,
00594 const QTextBlockFormat &defaultFormat )
00595 {
00596 if ( format.alignment() != defaultFormat.alignment() ||
00597 format.indent() != defaultFormat.indent() ||
00598 format.textIndent() != defaultFormat.textIndent() )
00599 return true;
00600
00601 return false;
00602 }
00603
00605 static bool isSpecial( const QTextFormat &charFormat )
00606 {
00607 return charFormat.isFrameFormat() || charFormat.isImageFormat() ||
00608 charFormat.isListFormat() || charFormat.isTableFormat();
00609 }
00610
00611 bool TextEdit::isFormattingUsed() const
00612 {
00613 if ( textMode() == Plain )
00614 return false;
00615
00616
00617
00618
00619
00620
00621
00622
00623
00624
00625
00626 QTextEdit defaultTextEdit;
00627 QTextCharFormat defaultCharFormat = defaultTextEdit.document()->begin().charFormat();
00628 QTextBlockFormat defaultBlockFormat = defaultTextEdit.document()->begin().blockFormat();
00629 QFont defaultFont = document()->defaultFont();
00630
00631 QTextBlock block = document()->firstBlock();
00632 while ( block.isValid() ) {
00633
00634 if ( isBlockFormatFormatted( block.blockFormat(), defaultBlockFormat ) ) {
00635 return true;
00636 }
00637
00638 if ( isSpecial( block.charFormat() ) || isSpecial( block.blockFormat() ) ||
00639 block.textList() ) {
00640 return true;
00641 }
00642
00643 QTextBlock::iterator it = block.begin();
00644 while ( !it.atEnd() ) {
00645 QTextFragment fragment = it.fragment();
00646 QTextCharFormat charFormat = fragment.charFormat();
00647 if ( isSpecial( charFormat ) ) {
00648 return true;
00649 }
00650 if ( isCharFormatFormatted( fragment.charFormat(), defaultFont, defaultCharFormat ) ) {
00651 return true;
00652 }
00653
00654 it++;
00655 }
00656 block = block.next();
00657 }
00658
00659 if ( toHtml().contains( QLatin1String( "<hr />" ) ) )
00660 return true;
00661
00662 return false;
00663 }
00664
00665 void TextEditPrivate::_k_slotDeleteLine()
00666 {
00667 q->deleteCurrentLine();
00668 }
00669
00670 void TextEdit::deleteCurrentLine()
00671 {
00672 QTextCursor cursor = textCursor();
00673 QTextBlock block = cursor.block();
00674 const QTextLayout* layout = block.layout();
00675
00676
00677
00678 for ( int lineNumber = 0; lineNumber < layout->lineCount(); lineNumber++ ) {
00679 QTextLine line = layout->lineAt( lineNumber );
00680 const bool lastLineInBlock = ( line.textStart() + line.textLength() == block.length() - 1 );
00681 const bool oneLineBlock = ( layout->lineCount() == 1 );
00682 const int startOfLine = block.position() + line.textStart();
00683 int endOfLine = block.position() + line.textStart() + line.textLength();
00684 if ( !lastLineInBlock )
00685 endOfLine -= 1;
00686
00687
00688 if ( cursor.position() >= startOfLine && cursor.position() <= endOfLine ) {
00689 int deleteStart = startOfLine;
00690 int deleteLength = line.textLength();
00691 if ( oneLineBlock )
00692 deleteLength++;
00693
00694
00695
00696 if ( deleteStart + deleteLength >= document()->characterCount() &&
00697 deleteStart > 0 )
00698 deleteStart--;
00699
00700 cursor.beginEditBlock();
00701 cursor.setPosition( deleteStart );
00702 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength );
00703 cursor.removeSelectedText();
00704 cursor.endEditBlock();
00705 return;
00706 }
00707 }
00708
00709 }
00710
00711
00712 #include "textedit.moc"