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 if ( o == this )
00177 KCursor::autoHideEventFilter( o, e );
00178 return KRichTextWidget::eventFilter( o, e );
00179 }
00180
00181 void TextEditPrivate::init()
00182 {
00183 q->setSpellInterface( q );
00184
00185
00186
00187
00188
00189
00190
00191 spellCheckingEnabled = false;
00192 q->setCheckSpellingEnabledInternal( true );
00193
00194 KCursor::setAutoHideCursor( q, true, true );
00195 q->installEventFilter( q );
00196 }
00197
00198 QString TextEdit::configFile() const
00199 {
00200 return d->configFile;
00201 }
00202
00203
00204 void TextEdit::keyPressEvent ( QKeyEvent * e )
00205 {
00206 if ( e->key() == Qt::Key_Return ) {
00207 QTextCursor cursor = textCursor();
00208 int oldPos = cursor.position();
00209 int blockPos = cursor.block().position();
00210
00211
00212 cursor.movePosition( QTextCursor::StartOfBlock );
00213 cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
00214 QString lineText = cursor.selectedText();
00215 if ( ( ( oldPos -blockPos ) > 0 ) &&
00216 ( ( oldPos-blockPos ) < int( lineText.length() ) ) ) {
00217 bool isQuotedLine = false;
00218 int bot = 0;
00219 while ( bot < lineText.length() ) {
00220 if( ( lineText[bot] == QChar::fromAscii( '>' ) ) ||
00221 ( lineText[bot] == QChar::fromAscii( '|' ) ) ) {
00222 isQuotedLine = true;
00223 ++bot;
00224 }
00225 else if ( lineText[bot].isSpace() ) {
00226 ++bot;
00227 }
00228 else {
00229 break;
00230 }
00231 }
00232 KRichTextWidget::keyPressEvent( e );
00233
00234
00235
00236 if ( isQuotedLine
00237 && ( bot != lineText.length() )
00238 && ( ( oldPos-blockPos ) >= int( bot ) ) ) {
00239
00240
00241 cursor.movePosition( QTextCursor::StartOfBlock );
00242 cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
00243 QString newLine = cursor.selectedText();
00244
00245
00246
00247 int leadingWhiteSpaceCount = 0;
00248 while ( ( leadingWhiteSpaceCount < newLine.length() )
00249 && newLine[leadingWhiteSpaceCount].isSpace() ) {
00250 ++leadingWhiteSpaceCount;
00251 }
00252 newLine = newLine.replace( 0, leadingWhiteSpaceCount,
00253 lineText.left( bot ) );
00254 cursor.insertText( newLine );
00255
00256 cursor.movePosition( QTextCursor::StartOfBlock );
00257 setTextCursor( cursor );
00258 }
00259 }
00260 else
00261 KRichTextWidget::keyPressEvent( e );
00262 }
00263 else
00264 {
00265 KRichTextWidget::keyPressEvent( e );
00266 }
00267 }
00268
00269
00270 bool TextEdit::isSpellCheckingEnabled() const
00271 {
00272 return d->spellCheckingEnabled;
00273 }
00274
00275 void TextEdit::setSpellCheckingEnabled( bool enable )
00276 {
00277 EMailQuoteHighlighter *hlighter =
00278 dynamic_cast<EMailQuoteHighlighter*>( highlighter() );
00279 if ( hlighter )
00280 hlighter->toggleSpellHighlighting( enable );
00281
00282 d->spellCheckingEnabled = enable;
00283 emit checkSpellingChanged( enable );
00284 }
00285
00286 bool TextEdit::shouldBlockBeSpellChecked( const QString& block ) const
00287 {
00288 return !isLineQuoted( block );
00289 }
00290
00291 bool KPIMTextEdit::TextEdit::isLineQuoted( const QString& line ) const
00292 {
00293 return quoteLength( line ) > 0;
00294 }
00295
00296 int KPIMTextEdit::TextEdit::quoteLength( const QString& line ) const
00297 {
00298 bool quoteFound = false;
00299 int startOfText = -1;
00300 for ( int i = 0; i < line.length(); i++ ) {
00301 if ( line[i] == QLatin1Char( '>' ) || line[i] == QLatin1Char( '|' ) )
00302 quoteFound = true;
00303 else if ( line[i] != QLatin1Char( ' ' ) ) {
00304 startOfText = i;
00305 break;
00306 }
00307 }
00308 if ( quoteFound ) {
00309 if ( startOfText == -1 )
00310 startOfText = line.length() - 1;
00311 return startOfText;
00312 }
00313 else
00314 return 0;
00315 }
00316
00317 const QString KPIMTextEdit::TextEdit::defaultQuoteSign() const
00318 {
00319 return QLatin1String( "> " );
00320 }
00321
00322 void TextEdit::createHighlighter()
00323 {
00324 EMailQuoteHighlighter *emailHighLighter =
00325 new EMailQuoteHighlighter( this );
00326
00327 setHighlighterColors( emailHighLighter );
00328
00329
00330 KRichTextWidget::setHighlighter( emailHighLighter );
00331
00332 if ( !spellCheckingLanguage().isEmpty() )
00333 setSpellCheckingLanguage( spellCheckingLanguage() );
00334 setSpellCheckingEnabled( isSpellCheckingEnabled() );
00335 }
00336
00337 void TextEdit::setHighlighterColors( EMailQuoteHighlighter *highlighter )
00338 {
00339 Q_UNUSED( highlighter );
00340 }
00341
00342 QString TextEdit::toWrappedPlainText() const
00343 {
00344 QString temp;
00345 QTextDocument* doc = document();
00346 QTextBlock block = doc->begin();
00347 while ( block.isValid() ) {
00348 QTextLayout* layout = block.layout();
00349 for ( int i = 0; i < layout->lineCount(); i++ ) {
00350 QTextLine line = layout->lineAt( i );
00351 temp += block.text().mid( line.textStart(), line.textLength() ) + QLatin1Char( '\n' );
00352 }
00353 block = block.next();
00354 }
00355
00356
00357 if ( temp.endsWith( QLatin1Char( '\n' ) ) )
00358 temp.chop( 1 );
00359
00360 d->fixupTextEditString( temp );
00361 return temp;
00362 }
00363
00364 QString TextEdit::toCleanPlainText() const
00365 {
00366 QString temp = toPlainText();
00367 d->fixupTextEditString( temp );
00368 return temp;
00369 }
00370
00371 void TextEdit::createActions( KActionCollection *actionCollection )
00372 {
00373 KRichTextWidget::createActions( actionCollection );
00374
00375 if ( d->imageSupportEnabled ) {
00376 d->actionAddImage = new KAction( KIcon( QLatin1String( "insert-image" ) ),
00377 i18n( "Add Image" ), this );
00378 actionCollection->addAction( QLatin1String( "add_image" ), d->actionAddImage );
00379 connect( d->actionAddImage, SIGNAL(triggered(bool) ), SLOT( _k_slotAddImage() ) );
00380 }
00381
00382 d->actionDeleteLine = new KAction( i18n( "Delete Line" ), this );
00383 d->actionDeleteLine->setShortcut( QKeySequence( Qt::CTRL + Qt::Key_K ) );
00384 actionCollection->addAction( QLatin1String( "delete_line" ), d->actionDeleteLine );
00385 connect( d->actionDeleteLine, SIGNAL(triggered(bool)), SLOT(_k_slotDeleteLine()) );
00386 }
00387
00388 void TextEdit::addImage( const KUrl &url )
00389 {
00390 QImage image;
00391 if ( !image.load( url.path() ) ) {
00392 KMessageBox::error( this,
00393 i18nc( "@info", "Unable to load image <filename>%1</filename>.", url.path() ) );
00394 return;
00395 }
00396 QFileInfo fi( url.path() );
00397 QString imageName = fi.baseName().isEmpty() ? QLatin1String( "image.png" )
00398 : fi.baseName() + QLatin1String( ".png" );
00399 d->addImageHelper( imageName, image );
00400 }
00401
00402 void TextEdit::loadImage ( const QImage& image, const QString& matchName, const QString& resourceName )
00403 {
00404 QSet<int> cursorPositionsToSkip;
00405 QTextBlock currentBlock = document()->begin();
00406 QTextBlock::iterator it;
00407 while ( currentBlock.isValid() ) {
00408 for (it = currentBlock.begin(); !(it.atEnd()); ++it) {
00409 QTextFragment fragment = it.fragment();
00410 if ( fragment.isValid() ) {
00411 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
00412 if ( imageFormat.isValid() && imageFormat.name() == matchName ) {
00413 int pos = fragment.position();
00414 if ( !cursorPositionsToSkip.contains( pos ) ) {
00415 QTextCursor cursor( document() );
00416 cursor.setPosition( pos );
00417 cursor.setPosition( pos + 1, QTextCursor::KeepAnchor );
00418 cursor.removeSelectedText();
00419 document()->addResource( QTextDocument::ImageResource, QUrl( resourceName ), QVariant( image ) );
00420 cursor.insertImage( resourceName );
00421
00422
00423
00424 cursorPositionsToSkip.insert( pos );
00425 it = currentBlock.begin();
00426 }
00427 }
00428 }
00429 }
00430 currentBlock = currentBlock.next();
00431 }
00432 }
00433
00434 void TextEditPrivate::addImageHelper( const QString &imageName, const QImage &image )
00435 {
00436 QString imageNameToAdd = imageName;
00437 QTextDocument *document = q->document();
00438
00439
00440 int imageNumber = 1;
00441 while ( mImageNames.contains( imageNameToAdd ) ) {
00442 QVariant qv = document->resource( QTextDocument::ImageResource, QUrl( imageNameToAdd ) );
00443 if ( qv == image ) {
00444
00445 break;
00446 }
00447 int firstDot = imageName.indexOf( QLatin1Char( '.' ) );
00448 if ( firstDot == -1 )
00449 imageNameToAdd = imageName + QString::number( imageNumber++ );
00450 else
00451 imageNameToAdd = imageName.left( firstDot ) + QString::number( imageNumber++ ) +
00452 imageName.mid( firstDot );
00453 }
00454
00455 if ( !mImageNames.contains( imageNameToAdd ) ) {
00456 document->addResource( QTextDocument::ImageResource, QUrl( imageNameToAdd ), image );
00457 mImageNames << imageNameToAdd;
00458 }
00459 q->textCursor().insertImage( imageNameToAdd );
00460 q->enableRichTextMode();
00461 }
00462
00463 ImageWithNameList TextEdit::imagesWithName() const
00464 {
00465 ImageWithNameList retImages;
00466 QStringList seenImageNames;
00467 QList<QTextImageFormat> imageFormats = d->embeddedImageFormats();
00468 foreach( const QTextImageFormat &imageFormat, imageFormats ) {
00469 if ( !seenImageNames.contains( imageFormat.name() ) ) {
00470 QVariant resourceData = document()->resource( QTextDocument::ImageResource, QUrl( imageFormat.name() ) );
00471 QImage image = qvariant_cast<QImage>( resourceData );
00472 QString name = imageFormat.name();
00473 ImageWithNamePtr newImage( new ImageWithName );
00474 newImage->image = image;
00475 newImage->name = name;
00476 retImages.append( newImage );
00477 seenImageNames.append( imageFormat.name() );
00478 }
00479 }
00480 return retImages;
00481 }
00482
00483 QList< QSharedPointer<EmbeddedImage> > TextEdit::embeddedImages() const
00484 {
00485 ImageWithNameList normalImages = imagesWithName();
00486 QList< QSharedPointer<EmbeddedImage> > retImages;
00487 foreach( const ImageWithNamePtr &normalImage, normalImages ) {
00488 QBuffer buffer;
00489 buffer.open( QIODevice::WriteOnly );
00490 normalImage->image.save( &buffer, "PNG" );
00491
00492 qsrand( QDateTime::currentDateTime().toTime_t() + qHash( normalImage->name ) );
00493 QSharedPointer<EmbeddedImage> embeddedImage( new EmbeddedImage() );
00494 retImages.append( embeddedImage );
00495 embeddedImage->image = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() );
00496 embeddedImage->imageName = normalImage->name;
00497 embeddedImage->contentID = QString( QLatin1String( "%1@KDE" ) ).arg( qrand() );
00498 }
00499 return retImages;
00500 }
00501
00502 QList<QTextImageFormat> TextEditPrivate::embeddedImageFormats() const
00503 {
00504 QTextDocument *doc = q->document();
00505 QList<QTextImageFormat> retList;
00506
00507 QTextBlock currentBlock = doc->begin();
00508 while ( currentBlock.isValid() ) {
00509 QTextBlock::iterator it;
00510 for ( it = currentBlock.begin(); !it.atEnd(); ++it ) {
00511 QTextFragment fragment = it.fragment();
00512 if ( fragment.isValid() ) {
00513 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
00514 if ( imageFormat.isValid() ) {
00515 retList.append( imageFormat );
00516 }
00517 }
00518 }
00519 currentBlock = currentBlock.next();
00520 }
00521 return retList;
00522 }
00523
00524 void TextEditPrivate::_k_slotAddImage()
00525 {
00526 QPointer<KFileDialog> fdlg = new KFileDialog( QString(), QString(), q );
00527 fdlg->setOperationMode( KFileDialog::Other );
00528 fdlg->setCaption( i18n("Add Image") );
00529 fdlg->okButton()->setGuiItem( KGuiItem( i18n("&Add"), QLatin1String( "document-open" ) ) );
00530 fdlg->setMode( KFile::Files );
00531 if ( fdlg->exec() != KDialog::Accepted ) {
00532 delete fdlg;
00533 return;
00534 }
00535
00536 const KUrl::List files = fdlg->selectedUrls();
00537 foreach ( const KUrl& url, files ) {
00538 q->addImage( url );
00539 }
00540 delete fdlg;
00541 }
00542
00543 void KPIMTextEdit::TextEdit::enableImageActions()
00544 {
00545 d->imageSupportEnabled = true;
00546 }
00547
00548 QByteArray KPIMTextEdit::TextEdit::imageNamesToContentIds( const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList )
00549 {
00550 QByteArray result = htmlBody;
00551 if ( imageList.size() > 0 ) {
00552 foreach( const QSharedPointer<EmbeddedImage> &image, imageList ) {
00553 const QString newImageName = QLatin1String( "cid:" ) + image->contentID;
00554 QByteArray quote( "\"" );
00555 result.replace( QByteArray( quote + image->imageName.toLocal8Bit() + quote ),
00556 QByteArray( quote + newImageName.toLocal8Bit() + quote ) );
00557 }
00558 }
00559 return result;
00560 }
00561
00562 void TextEdit::insertFromMimeData( const QMimeData *source )
00563 {
00564
00565 if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) {
00566 QImage image = qvariant_cast<QImage>( source->imageData() );
00567 QFileInfo fi( source->text() );
00568 QString imageName = fi.baseName().isEmpty() ? i18nc( "Start of the filename for an image", "image" ) : fi.baseName();
00569 d->addImageHelper( imageName, image );
00570 return;
00571 }
00572
00573
00574
00575 if ( textMode() == KRichTextEdit::Plain && source->hasHtml() ) {
00576 if ( source->hasText() ) {
00577 insertPlainText( source->text() );
00578 return;
00579 }
00580 }
00581
00582 KRichTextWidget::insertFromMimeData( source );
00583 }
00584
00585 bool KPIMTextEdit::TextEdit::canInsertFromMimeData( const QMimeData *source ) const
00586 {
00587 if ( source->hasHtml() && textMode() == KRichTextEdit::Rich )
00588 return true;
00589 if ( source->hasText() )
00590 return true;
00591 if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled )
00592 return true;
00593
00594 return KRichTextWidget::canInsertFromMimeData( source );
00595 }
00596
00597 bool TextEdit::isFormattingUsed() const
00598 {
00599 if ( textMode() == Plain )
00600 return false;
00601
00602 return TextUtils::containsFormatting( document() );
00603 }
00604
00605 void TextEditPrivate::_k_slotDeleteLine()
00606 {
00607 q->deleteCurrentLine();
00608 }
00609
00610 void TextEdit::deleteCurrentLine()
00611 {
00612 QTextCursor cursor = textCursor();
00613 QTextBlock block = cursor.block();
00614 const QTextLayout* layout = block.layout();
00615
00616
00617
00618 for ( int lineNumber = 0; lineNumber < layout->lineCount(); lineNumber++ ) {
00619 QTextLine line = layout->lineAt( lineNumber );
00620 const bool lastLineInBlock = ( line.textStart() + line.textLength() == block.length() - 1 );
00621 const bool oneLineBlock = ( layout->lineCount() == 1 );
00622 const int startOfLine = block.position() + line.textStart();
00623 int endOfLine = block.position() + line.textStart() + line.textLength();
00624 if ( !lastLineInBlock )
00625 endOfLine -= 1;
00626
00627
00628 if ( cursor.position() >= startOfLine && cursor.position() <= endOfLine ) {
00629 int deleteStart = startOfLine;
00630 int deleteLength = line.textLength();
00631 if ( oneLineBlock )
00632 deleteLength++;
00633
00634
00635
00636 if ( deleteStart + deleteLength >= document()->characterCount() &&
00637 deleteStart > 0 )
00638 deleteStart--;
00639
00640 cursor.beginEditBlock();
00641 cursor.setPosition( deleteStart );
00642 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength );
00643 cursor.removeSelectedText();
00644 cursor.endEditBlock();
00645 return;
00646 }
00647 }
00648
00649 }
00650
00651
00652 #include "textedit.moc"