diff --git a/kpimtextedit/textedit.cpp b/kpimtextedit/textedit.cpp index 1ddf9205b..092aa71a0 100644 --- a/kpimtextedit/textedit.cpp +++ b/kpimtextedit/textedit.cpp @@ -1,704 +1,712 @@ /* Copyright (c) 2009 Thomas McGuire Based on KMail and libkdepim code by: Copyright 2007 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "textedit.h" #include "emailquotehighlighter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KPIMTextEdit { class TextEditPrivate { public: TextEditPrivate( TextEdit *parent ) : actionAddImage( 0 ), actionDeleteLine( 0 ), q( parent ), imageSupportEnabled( false ) { } /** * Helper function for addImage(), which does the actual work of adding the QImage as a * resource to the document, pasting it and adding it to the image name list. * * @param imageName the desired image name. If it is already taken, a number will * be appended to it * @param image the actual image to add */ void addImageHelper( const QString &imageName, const QImage &image ); /** * Helper function to get the list of all QTextImageFormats in the document. */ QList embeddedImageFormats() const; /** * Removes embedded image markers, converts non-breaking spaces to normal spaces * and other fixes for strings that came from toPlainText()/toHtml(). */ void fixupTextEditString( QString &text ) const; /** * Does the constructor work */ void init(); /** * Opens a file dialog to let the user choose an image and then pastes that * image to the editor */ void _k_slotAddImage(); void _k_slotDeleteLine(); /// The action that triggers _k_slotAddImage() KAction *actionAddImage; /// The action that triggers _k_slotDeleteLine() KAction *actionDeleteLine; /// The parent class TextEdit *q; /// Whether or not adding or pasting images is supported bool imageSupportEnabled; /** * The names of embedded images. * Used to easily obtain the names of the images. * New images are compared to the the list and not added as resource if already present. */ QStringList mImageNames; /** * Although KTextEdit keeps track of the spell checking state, we override * it here, because we have a highlighter which does quote highlighting. * And since disabling spellchecking in KTextEdit simply would turn off our * quote highlighter, we never actually deactivate spell checking in the * base class, but only tell our own email highlighter to not highlight * spelling mistakes. * For this, we use the KTextEditSpellInterface, which is basically a hack * that makes it possible to have our own enabled/disabled state in a binary * compatible way. */ bool spellCheckingEnabled; }; } // namespace using namespace KPIMTextEdit; void TextEditPrivate::fixupTextEditString( QString &text ) const { // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here text.remove( QChar::LineSeparator ); // Get rid of embedded images, see QTextImageFormat documentation: // "Inline images are represented by an object replacement character (0xFFFC in Unicode) " text.remove( 0xFFFC ); // In plaintext mode, each space is non-breaking. text.replace( QChar::Nbsp, QChar::fromAscii( ' ' ) ); } TextEdit::TextEdit( const QString& text, QWidget *parent ) : KRichTextWidget( text, parent ), d( new TextEditPrivate( this ) ) { d->init(); } TextEdit::TextEdit( QWidget *parent ) : KRichTextWidget( parent ), d( new TextEditPrivate( this ) ) { d->init(); } TextEdit::~TextEdit() { } bool TextEdit::eventFilter( QObject*o, QEvent* e ) { if ( o == this ) KCursor::autoHideEventFilter( o, e ); return KRichTextWidget::eventFilter( o, e ); } void TextEditPrivate::init() { q->setSpellInterface( q ); // We tell the KRichTextWidget to enable spell checking, because only then it will // call createHighlighter() which will create our own highlighter which also // does quote highlighting. // However, *our* spellchecking is still disabled. Our own highlighter only // cares about our spellcheck status, it will not highlight missspelled words // if our spellchecking is disabled. // See also KEMailQuotingHighlighter::highlightBlock(). spellCheckingEnabled = false; q->setCheckSpellingEnabledInternal( true ); KCursor::setAutoHideCursor( q, true, true ); q->installEventFilter( q ); } void TextEdit::keyPressEvent ( QKeyEvent * e ) { if ( e->key() == Qt::Key_Return ) { QTextCursor cursor = textCursor(); int oldPos = cursor.position(); int blockPos = cursor.block().position(); //selection all the line. cursor.movePosition( QTextCursor::StartOfBlock ); cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); QString lineText = cursor.selectedText(); if ( ( ( oldPos -blockPos ) > 0 ) && ( ( oldPos-blockPos ) < int( lineText.length() ) ) ) { bool isQuotedLine = false; int bot = 0; // bot = begin of text after quote indicators while ( bot < lineText.length() ) { if( ( lineText[bot] == QChar::fromAscii( '>' ) ) || ( lineText[bot] == QChar::fromAscii( '|' ) ) ) { isQuotedLine = true; ++bot; } else if ( lineText[bot].isSpace() ) { ++bot; } else { break; } } KRichTextWidget::keyPressEvent( e ); // duplicate quote indicators of the previous line before the new // line if the line actually contained text (apart from the quote // indicators) and the cursor is behind the quote indicators if ( isQuotedLine && ( bot != lineText.length() ) && ( ( oldPos-blockPos ) >= int( bot ) ) ) { // The cursor position might have changed unpredictably if there was selected // text which got replaced by a new line, so we query it again: cursor.movePosition( QTextCursor::StartOfBlock ); cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); QString newLine = cursor.selectedText(); // remove leading white space from the new line and instead // add the quote indicators of the previous line int leadingWhiteSpaceCount = 0; while ( ( leadingWhiteSpaceCount < newLine.length() ) && newLine[leadingWhiteSpaceCount].isSpace() ) { ++leadingWhiteSpaceCount; } newLine = newLine.replace( 0, leadingWhiteSpaceCount, lineText.left( bot ) ); cursor.insertText( newLine ); //cursor.setPosition( cursor.position() + 2); cursor.movePosition( QTextCursor::StartOfBlock ); setTextCursor( cursor ); } } else KRichTextWidget::keyPressEvent( e ); } else { KRichTextWidget::keyPressEvent( e ); } } bool TextEdit::isSpellCheckingEnabled() const { return d->spellCheckingEnabled; } void TextEdit::setSpellCheckingEnabled( bool enable ) { EMailQuoteHighlighter *hlighter = dynamic_cast( highlighter() ); if ( hlighter ) hlighter->toggleSpellHighlighting( enable ); d->spellCheckingEnabled = enable; emit checkSpellingChanged( enable ); } bool TextEdit::shouldBlockBeSpellChecked( const QString& block ) const { return !isLineQuoted( block ); } bool KPIMTextEdit::TextEdit::isLineQuoted( const QString& line ) const { return quoteLength( line ) > 0; } int KPIMTextEdit::TextEdit::quoteLength( const QString& line ) const { bool quoteFound = false; int startOfText = -1; for ( int i = 0; i < line.length(); i++ ) { if ( line[i] == QLatin1Char( '>' ) || line[i] == QLatin1Char( '|' ) ) quoteFound = true; else if ( line[i] != QLatin1Char( ' ' ) ) { startOfText = i; break; } } if ( quoteFound ) { if ( startOfText == -1 ) startOfText = line.length() - 1; return startOfText; } else return 0; } const QString KPIMTextEdit::TextEdit::defaultQuoteSign() const { return QLatin1String( "> " ); } void TextEdit::createHighlighter() { EMailQuoteHighlighter *emailHighLighter = new EMailQuoteHighlighter( this ); setHighlighterColors( emailHighLighter ); //TODO change config KRichTextWidget::setHighlighter( emailHighLighter ); if ( !spellCheckingLanguage().isEmpty() ) setSpellCheckingLanguage( spellCheckingLanguage() ); setSpellCheckingEnabled( isSpellCheckingEnabled() ); } void TextEdit::setHighlighterColors( EMailQuoteHighlighter *highlighter ) { Q_UNUSED( highlighter ); } QString TextEdit::toWrappedPlainText() const { QString temp; QTextDocument* doc = document(); QTextBlock block = doc->begin(); while ( block.isValid() ) { QTextLayout* layout = block.layout(); for ( int i = 0; i < layout->lineCount(); i++ ) { QTextLine line = layout->lineAt( i ); temp += block.text().mid( line.textStart(), line.textLength() ) + QLatin1Char( '\n' ); } block = block.next(); } // Remove the last superfluous newline added above if ( temp.endsWith( QLatin1Char( '\n' ) ) ) temp.chop( 1 ); d->fixupTextEditString( temp ); return temp; } QString TextEdit::toCleanPlainText() const { QString temp = toPlainText(); d->fixupTextEditString( temp ); return temp; } void TextEdit::createActions( KActionCollection *actionCollection ) { KRichTextWidget::createActions( actionCollection ); if ( d->imageSupportEnabled ) { d->actionAddImage = new KAction( KIcon( QLatin1String( "insert-image" ) ), i18n( "Add Image" ), this ); actionCollection->addAction( QLatin1String( "add_image" ), d->actionAddImage ); connect( d->actionAddImage, SIGNAL(triggered(bool) ), SLOT( _k_slotAddImage() ) ); } d->actionDeleteLine = new KAction( i18n( "Delete Line" ), this ); d->actionDeleteLine->setShortcut( QKeySequence( Qt::CTRL + Qt::Key_K ) ); actionCollection->addAction( QLatin1String( "delete_line" ), d->actionDeleteLine ); connect( d->actionDeleteLine, SIGNAL(triggered(bool)), SLOT(_k_slotDeleteLine()) ); } void TextEdit::addImage( const KUrl &url ) { QImage image; if ( !image.load( url.path() ) ) { KMessageBox::error( this, i18nc( "@info", "Unable to load image %1.", url.path() ) ); return; } QFileInfo fi( url.path() ); QString imageName = fi.baseName().isEmpty() ? QLatin1String( "image.png" ) : fi.baseName() + QLatin1String( ".png" ); d->addImageHelper( imageName, image ); } void TextEdit::loadImage ( const QImage& image, const QString& matchName, const QString& resourceName ) { + QSet cursorPositionsToSkip; QTextBlock currentBlock = document()->begin(); QTextBlock::iterator it; while ( currentBlock.isValid() ) { for (it = currentBlock.begin(); !(it.atEnd()); ++it) { QTextFragment fragment = it.fragment(); if ( fragment.isValid() ) { QTextImageFormat imageFormat = fragment.charFormat().toImageFormat(); if ( imageFormat.isValid() && imageFormat.name() == matchName ) { int pos = fragment.position(); - QTextCursor cursor( document() ); - cursor.setPosition( pos ); - cursor.setPosition( pos + 1, QTextCursor::KeepAnchor ); - cursor.removeSelectedText(); - document()->addResource( QTextDocument::ImageResource, QUrl( resourceName ), QVariant( image ) ); - cursor.insertImage( resourceName ); + if ( !cursorPositionsToSkip.contains( pos ) ) { + QTextCursor cursor( document() ); + cursor.setPosition( pos ); + cursor.setPosition( pos + 1, QTextCursor::KeepAnchor ); + cursor.removeSelectedText(); + document()->addResource( QTextDocument::ImageResource, QUrl( resourceName ), QVariant( image ) ); + cursor.insertImage( resourceName ); + + // The textfragment iterator is now invalid, restart from the beginning + // Take care not to replace the same fragment again, or we would be in an infinite loop. + cursorPositionsToSkip.insert( pos ); + it = currentBlock.begin(); + } } } } currentBlock = currentBlock.next(); } } void TextEditPrivate::addImageHelper( const QString &imageName, const QImage &image ) { QString imageNameToAdd = imageName; QTextDocument *document = q->document(); // determine the imageNameToAdd int imageNumber = 1; while ( mImageNames.contains( imageNameToAdd ) ) { QVariant qv = document->resource( QTextDocument::ImageResource, QUrl( imageNameToAdd ) ); if ( qv == image ) { // use the same name break; } int firstDot = imageName.indexOf( QLatin1Char( '.' ) ); if ( firstDot == -1 ) imageNameToAdd = imageName + QString::number( imageNumber++ ); else imageNameToAdd = imageName.left( firstDot ) + QString::number( imageNumber++ ) + imageName.mid( firstDot ); } if ( !mImageNames.contains( imageNameToAdd ) ) { document->addResource( QTextDocument::ImageResource, QUrl( imageNameToAdd ), image ); mImageNames << imageNameToAdd; } q->textCursor().insertImage( imageNameToAdd ); q->enableRichTextMode(); } ImageWithNameList TextEdit::imagesWithName() const { ImageWithNameList retImages; QStringList seenImageNames; QList imageFormats = d->embeddedImageFormats(); foreach( const QTextImageFormat &imageFormat, imageFormats ) { if ( !seenImageNames.contains( imageFormat.name() ) ) { QVariant data = document()->resource( QTextDocument::ImageResource, QUrl( imageFormat.name() ) ); QImage image = qvariant_cast( data ); QString name = imageFormat.name(); ImageWithNamePtr newImage( new ImageWithName ); newImage->image = image; newImage->name = name; retImages.append( newImage ); seenImageNames.append( imageFormat.name() ); } } return retImages; } QList< QSharedPointer > TextEdit::embeddedImages() const { ImageWithNameList normalImages = imagesWithName(); QList< QSharedPointer > retImages; foreach( const ImageWithNamePtr &normalImage, normalImages ) { QBuffer buffer; buffer.open( QIODevice::WriteOnly ); normalImage->image.save( &buffer, "PNG" ); qsrand( QDateTime::currentDateTime().toTime_t() + qHash( normalImage->name ) ); QSharedPointer embeddedImage( new EmbeddedImage() ); retImages.append( embeddedImage ); embeddedImage->image = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() ); embeddedImage->imageName = normalImage->name; embeddedImage->contentID = QString( QLatin1String( "%1@KDE" ) ).arg( qrand() ); } return retImages; } QList TextEditPrivate::embeddedImageFormats() const { QTextDocument *doc = q->document(); QList retList; QTextBlock currentBlock = doc->begin(); while ( currentBlock.isValid() ) { QTextBlock::iterator it; for ( it = currentBlock.begin(); !it.atEnd(); ++it ) { QTextFragment fragment = it.fragment(); if ( fragment.isValid() ) { QTextImageFormat imageFormat = fragment.charFormat().toImageFormat(); if ( imageFormat.isValid() ) { retList.append( imageFormat ); } } } currentBlock = currentBlock.next(); } return retList; } void TextEditPrivate::_k_slotAddImage() { QPointer fdlg = new KFileDialog( QString(), QString(), q ); fdlg->setOperationMode( KFileDialog::Other ); fdlg->setCaption( i18n("Add Image") ); fdlg->okButton()->setGuiItem( KGuiItem( i18n("&Add"), QLatin1String( "document-open" ) ) ); fdlg->setMode( KFile::Files ); if ( fdlg->exec() != KDialog::Accepted ) { delete fdlg; return; } const KUrl::List files = fdlg->selectedUrls(); foreach ( const KUrl& url, files ) { q->addImage( url ); } delete fdlg; } void KPIMTextEdit::TextEdit::enableImageActions() { d->imageSupportEnabled = true; } QByteArray KPIMTextEdit::TextEdit::imageNamesToContentIds( const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList ) { QByteArray result = htmlBody; if ( imageList.size() > 0 ) { foreach( const QSharedPointer &image, imageList ) { const QString newImageName = QLatin1String( "cid:" ) + image->contentID; QByteArray quote( "\"" ); result.replace( QByteArray( quote + image->imageName.toLocal8Bit() + quote ), QByteArray( quote + newImageName.toLocal8Bit() + quote ) ); } } return result; } void TextEdit::insertFromMimeData( const QMimeData *source ) { // Add an image if that is on the clipboard if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) { QImage image = qvariant_cast( source->imageData() ); QFileInfo fi( source->text() ); QString imageName = fi.baseName().isEmpty() ? i18nc( "Start of the filename for an image", "image" ) : fi.baseName(); d->addImageHelper( imageName, image ); return; } // Attempt to paste HTML contents into the text edit in plain text mode, // prevent this and prevent plain text instead. if ( textMode() == KRichTextEdit::Plain && source->hasHtml() ) { if ( source->hasText() ) { insertPlainText( source->text() ); return; } } KRichTextWidget::insertFromMimeData( source ); } bool KPIMTextEdit::TextEdit::canInsertFromMimeData( const QMimeData *source ) const { if ( source->hasHtml() && textMode() == KRichTextEdit::Rich ) return true; if ( source->hasText() ) return true; if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) return true; return KRichTextWidget::canInsertFromMimeData( source ); } static bool isCharFormatFormatted( const QTextCharFormat &format, const QFont &defaultFont, const QTextCharFormat &defaultBlockFormat ) { if ( !format.anchorHref().isEmpty() || format.font() != defaultFont || format.isAnchor() || format.verticalAlignment() != defaultBlockFormat.verticalAlignment() || format.underlineStyle() != defaultBlockFormat.underlineStyle() || format.foreground().color() != defaultBlockFormat.foreground().color() || format.background().color() != defaultBlockFormat.background().color() ) return true; return false; } static bool isBlockFormatFormatted( const QTextBlockFormat &format, const QTextBlockFormat &defaultFormat ) { if ( format.alignment() != defaultFormat.alignment() || format.indent() != defaultFormat.indent() || format.textIndent() != defaultFormat.textIndent() ) return true; return false; } /// @return true if the format represents a list, table, image or something like that. static bool isSpecial( const QTextFormat &charFormat ) { return charFormat.isFrameFormat() || charFormat.isImageFormat() || charFormat.isListFormat() || charFormat.isTableFormat(); } bool TextEdit::isFormattingUsed() const { if ( textMode() == Plain ) return false; // Below, we walk through all text blocks and through all text fragments in them // and check if any of those has any formatting. // To check if they have formatting, we use the functions isBlockFormatFormatted() and // isCharFormatFormatted(). Those do not check all the exising formatting possibilities on // earth, but everything that KRichTextEdit supports at the moment. // // Also, we have to compare the formats against those of a default text edit. For example, // we can't compare the foreground color against black, because the user might have another // color scheme. Therefore we compare the foreground color against a default text edit. QTextEdit defaultTextEdit; QTextCharFormat defaultCharFormat = defaultTextEdit.document()->begin().charFormat(); QTextBlockFormat defaultBlockFormat = defaultTextEdit.document()->begin().blockFormat(); QFont defaultFont = document()->defaultFont(); QTextBlock block = document()->firstBlock(); while ( block.isValid() ) { if ( isBlockFormatFormatted( block.blockFormat(), defaultBlockFormat ) ) { return true; } if ( isSpecial( block.charFormat() ) || isSpecial( block.blockFormat() ) || block.textList() ) { return true; } QTextBlock::iterator it = block.begin(); while ( !it.atEnd() ) { QTextFragment fragment = it.fragment(); QTextCharFormat charFormat = fragment.charFormat(); if ( isSpecial( charFormat ) ) { return true; } if ( isCharFormatFormatted( fragment.charFormat(), defaultFont, defaultCharFormat ) ) { return true; } it++; } block = block.next(); } if ( toHtml().contains( QLatin1String( "
" ) ) ) return true; return false; } void TextEditPrivate::_k_slotDeleteLine() { q->deleteCurrentLine(); } void TextEdit::deleteCurrentLine() { QTextCursor cursor = textCursor(); QTextBlock block = cursor.block(); const QTextLayout* layout = block.layout(); // The current text block can have several lines due to word wrapping. // Search the line the cursor is in, and then delete it. for ( int lineNumber = 0; lineNumber < layout->lineCount(); lineNumber++ ) { QTextLine line = layout->lineAt( lineNumber ); const bool lastLineInBlock = ( line.textStart() + line.textLength() == block.length() - 1 ); const bool oneLineBlock = ( layout->lineCount() == 1 ); const int startOfLine = block.position() + line.textStart(); int endOfLine = block.position() + line.textStart() + line.textLength(); if ( !lastLineInBlock ) endOfLine -= 1; // Found the line where the cursor is in if ( cursor.position() >= startOfLine && cursor.position() <= endOfLine ) { int deleteStart = startOfLine; int deleteLength = line.textLength(); if ( oneLineBlock ) deleteLength++; // The trailing newline // When deleting the last line in the document, // remove the newline of the line before the last line instead if ( deleteStart + deleteLength >= document()->characterCount() && deleteStart > 0 ) deleteStart--; cursor.beginEditBlock(); cursor.setPosition( deleteStart ); cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength ); cursor.removeSelectedText(); cursor.endEditBlock(); return; } } } #include "textedit.moc"