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 #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 }
00132
00133 using namespace KPIMTextEdit;
00134
00135 void TextEditPrivate::fixupTextEditString( QString &text ) const
00136 {
00137
00138 text.remove( QChar::LineSeparator );
00139
00140
00141
00142 text.remove( 0xFFFC );
00143
00144
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
00187
00188
00189
00190
00191
00192
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
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;
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
00238
00239
00240 if ( isQuotedLine
00241 && ( bot != lineText.length() )
00242 && ( ( oldPos-blockPos ) >= int( bot ) ) ) {
00243
00244
00245 cursor.movePosition( QTextCursor::StartOfBlock );
00246 cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
00247 QString newLine = cursor.selectedText();
00248
00249
00250
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
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
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
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 : 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
00427
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
00444 int imageNumber = 1;
00445 while ( mImageNames.contains( imageNameToAdd ) ) {
00446 QVariant qv = document->resource( QTextDocument::ImageResource, QUrl( imageNameToAdd ) );
00447 if ( qv == image ) {
00448
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
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
00588
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 q->deleteCurrentLine();
00622 }
00623
00624 void TextEdit::deleteCurrentLine()
00625 {
00626 QTextCursor cursor = textCursor();
00627 QTextBlock block = cursor.block();
00628 const QTextLayout* layout = block.layout();
00629
00630
00631
00632 for ( int lineNumber = 0; lineNumber < layout->lineCount(); lineNumber++ ) {
00633 QTextLine line = layout->lineAt( lineNumber );
00634 const bool lastLineInBlock = ( line.textStart() + line.textLength() == block.length() - 1 );
00635 const bool oneLineBlock = ( layout->lineCount() == 1 );
00636 const int startOfLine = block.position() + line.textStart();
00637 int endOfLine = block.position() + line.textStart() + line.textLength();
00638 if ( !lastLineInBlock )
00639 endOfLine -= 1;
00640
00641
00642 if ( cursor.position() >= startOfLine && cursor.position() <= endOfLine ) {
00643 int deleteStart = startOfLine;
00644 int deleteLength = line.textLength();
00645 if ( oneLineBlock )
00646 deleteLength++;
00647
00648
00649
00650 if ( deleteStart + deleteLength >= document()->characterCount() &&
00651 deleteStart > 0 )
00652 deleteStart--;
00653
00654 cursor.beginEditBlock();
00655 cursor.setPosition( deleteStart );
00656 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength );
00657 cursor.removeSelectedText();
00658 cursor.endEditBlock();
00659 return;
00660 }
00661 }
00662
00663 }
00664
00665
00666 #include "textedit.moc"