1/*
2 Copyright (c) 2009 Thomas McGuire <mcguire@kde.org>
3
4 Based on KMail and libkdepim code by:
5 Copyright 2007 - 2012 Laurent Montel <montel@kde.org>
6
7 This library is free software; you can redistribute it and/or modify it
8 under the terms of the GNU Library General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or (at your
10 option) any later version.
11
12 This library is distributed in the hope that it will be useful, but WITHOUT
13 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
15 License for more details.
16
17 You should have received a copy of the GNU Library General Public License
18 along with this library; see the file COPYING.LIB. If not, write to the
19 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
20 02110-1301, USA.
21*/
22#include "textedit.h"
23
24#include "emailquotehighlighter.h"
25#include "emoticontexteditaction.h"
26#include "inserthtmldialog.h"
27#include "tableactionmenu.h"
28#include "insertimagedialog.h"
29
30#include <kmime/kmime_codecs.h>
31
32#include <KDE/KAction>
33#include <KDE/KActionCollection>
34#include <KDE/KCursor>
35#include <KDE/KFileDialog>
36#include <KDE/KLocalizedString>
37#include <KDE/KMessageBox>
38#include <KDE/KPushButton>
39#include <KDE/KUrl>
40#include <KDE/KIcon>
41
42#include <QtCore/QBuffer>
43#include <QtCore/QDateTime>
44#include <QtCore/QMimeData>
45#include <QtCore/QFileInfo>
46#include <QtCore/QPointer>
47#include <QKeyEvent>
48#include <QTextLayout>
49
50#include "textutils.h"
51
52namespace KPIMTextEdit {
53
54class TextEditPrivate
55{
56 public:
57
58 TextEditPrivate( TextEdit *parent )
59 : actionAddImage( 0 ),
60 actionDeleteLine( 0 ),
61 actionAddEmoticon( 0 ),
62 actionInsertHtml( 0 ),
63 actionTable( 0 ),
64 actionFormatReset( 0 ),
65 q( parent ),
66 imageSupportEnabled( false ),
67 emoticonSupportEnabled( false ),
68 insertHtmlSupportEnabled( false ),
69 insertTableSupportEnabled( false ),
70 spellCheckingEnabled( false )
71 {
72 }
73
74 /**
75 * Helper function for addImage(), which does the actual work of adding the QImage as a
76 * resource to the document, pasting it and adding it to the image name list.
77 *
78 * @param imageName the desired image name. If it is already taken, a number will
79 * be appended to it
80 * @param image the actual image to add
81 */
82 void addImageHelper( const QString &imageName, const QImage &image,
83 int width = -1, int height = -1 );
84
85 /**
86 * Helper function to get the list of all QTextImageFormats in the document.
87 */
88 QList<QTextImageFormat> embeddedImageFormats() const;
89
90 /**
91 * Removes embedded image markers, converts non-breaking spaces to normal spaces
92 * and other fixes for strings that came from toPlainText()/toHtml().
93 */
94 void fixupTextEditString( QString &text ) const;
95
96 /**
97 * Does the constructor work
98 */
99 void init();
100
101 /**
102 * Opens a file dialog to let the user choose an image and then pastes that
103 * image to the editor
104 */
105 void _k_slotAddImage();
106
107 void _k_slotDeleteLine();
108
109 void _k_slotAddEmoticon( const QString & );
110
111 void _k_slotInsertHtml();
112
113 void _k_slotFormatReset();
114
115 void _k_slotTextModeChanged( KRichTextEdit::Mode );
116
117 /// The action that triggers _k_slotAddImage()
118 KAction *actionAddImage;
119
120 /// The action that triggers _k_slotDeleteLine()
121 KAction *actionDeleteLine;
122
123 EmoticonTextEditAction *actionAddEmoticon;
124
125 KAction *actionInsertHtml;
126
127 TableActionMenu *actionTable;
128
129 KAction *actionFormatReset;
130
131 /// The parent class
132 TextEdit *q;
133
134 /// Whether or not adding or pasting images is supported
135 bool imageSupportEnabled;
136
137 bool emoticonSupportEnabled;
138
139 bool insertHtmlSupportEnabled;
140
141 bool insertTableSupportEnabled;
142 /**
143 * The names of embedded images.
144 * Used to easily obtain the names of the images.
145 * New images are compared to the list and not added as resource if already present.
146 */
147 QStringList mImageNames;
148
149 /**
150 * Although KTextEdit keeps track of the spell checking state, we override
151 * it here, because we have a highlighter which does quote highlighting.
152 * And since disabling spellchecking in KTextEdit simply would turn off our
153 * quote highlighter, we never actually deactivate spell checking in the
154 * base class, but only tell our own email highlighter to not highlight
155 * spelling mistakes.
156 * For this, we use the KTextEditSpellInterface, which is basically a hack
157 * that makes it possible to have our own enabled/disabled state in a binary
158 * compatible way.
159 */
160 bool spellCheckingEnabled;
161
162 QString configFile;
163 QFont saveFont;
164};
165
166} // namespace
167
168using namespace KPIMTextEdit;
169
170void TextEditPrivate::fixupTextEditString( QString &text ) const
171{
172 // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here
173 text.remove( QChar::LineSeparator );
174
175 // Get rid of embedded images, see QTextImageFormat documentation:
176 // "Inline images are represented by an object replacement character (0xFFFC in Unicode) "
177 text.remove( 0xFFFC );
178
179 // In plaintext mode, each space is non-breaking.
180 text.replace( QChar::Nbsp, QChar::fromLatin1( ' ' ) );
181}
182
183TextEdit::TextEdit( const QString &text, QWidget *parent )
184 : KRichTextWidget( text, parent ),
185 d( new TextEditPrivate( this ) )
186{
187 d->init();
188}
189
190TextEdit::TextEdit( QWidget *parent )
191 : KRichTextWidget( parent ),
192 d( new TextEditPrivate( this ) )
193{
194 d->init();
195}
196
197TextEdit::TextEdit( QWidget *parent, const QString &configFile )
198 : KRichTextWidget( parent ),
199 d( new TextEditPrivate( this ) )
200{
201 d->init();
202 d->configFile = configFile;
203}
204
205TextEdit::~TextEdit()
206{
207}
208
209bool TextEdit::eventFilter( QObject *o, QEvent *e )
210{
211#ifndef QT_NO_CURSOR
212 if ( o == this ) {
213 KCursor::autoHideEventFilter( o, e );
214 }
215#endif
216 return KRichTextWidget::eventFilter( o, e );
217}
218
219void TextEditPrivate::init()
220{
221 q->connect( q, SIGNAL(textModeChanged(KRichTextEdit::Mode)),
222 q, SLOT(_k_slotTextModeChanged(KRichTextEdit::Mode)) );
223 q->setSpellInterface( q );
224 // We tell the KRichTextWidget to enable spell checking, because only then it will
225 // call createHighlighter() which will create our own highlighter which also
226 // does quote highlighting.
227 // However, *our* spellchecking is still disabled. Our own highlighter only
228 // cares about our spellcheck status, it will not highlight missspelled words
229 // if our spellchecking is disabled.
230 // See also KEMailQuotingHighlighter::highlightBlock().
231 spellCheckingEnabled = false;
232 q->setCheckSpellingEnabledInternal( true );
233
234#ifndef QT_NO_CURSOR
235 KCursor::setAutoHideCursor( q, true, true );
236#endif
237 q->installEventFilter( q );
238}
239
240QString TextEdit::configFile() const
241{
242 return d->configFile;
243}
244
245void TextEdit::keyPressEvent ( QKeyEvent * e )
246{
247 if ( e->key() == Qt::Key_Return ) {
248 QTextCursor cursor = textCursor();
249 int oldPos = cursor.position();
250 int blockPos = cursor.block().position();
251
252 //selection all the line.
253 cursor.movePosition( QTextCursor::StartOfBlock );
254 cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
255 QString lineText = cursor.selectedText();
256 if ( ( ( oldPos - blockPos ) > 0 ) &&
257 ( ( oldPos - blockPos ) < int( lineText.length() ) ) ) {
258 bool isQuotedLine = false;
259 int bot = 0; // bot = begin of text after quote indicators
260 while ( bot < lineText.length() ) {
261 if ( ( lineText[bot] == QChar::fromLatin1( '>' ) ) ||
262 ( lineText[bot] == QChar::fromLatin1( '|' ) ) ) {
263 isQuotedLine = true;
264 ++bot;
265 } else if ( lineText[bot].isSpace() ) {
266 ++bot;
267 } else {
268 break;
269 }
270 }
271 KRichTextWidget::keyPressEvent( e );
272 // duplicate quote indicators of the previous line before the new
273 // line if the line actually contained text (apart from the quote
274 // indicators) and the cursor is behind the quote indicators
275 if ( isQuotedLine &&
276 ( bot != lineText.length() ) &&
277 ( ( oldPos - blockPos ) >= int( bot ) ) ) {
278 // The cursor position might have changed unpredictably if there was selected
279 // text which got replaced by a new line, so we query it again:
280 cursor.movePosition( QTextCursor::StartOfBlock );
281 cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
282 QString newLine = cursor.selectedText();
283
284 // remove leading white space from the new line and instead
285 // add the quote indicators of the previous line
286 int leadingWhiteSpaceCount = 0;
287 while ( ( leadingWhiteSpaceCount < newLine.length() ) &&
288 newLine[leadingWhiteSpaceCount].isSpace() ) {
289 ++leadingWhiteSpaceCount;
290 }
291 newLine = newLine.replace( 0, leadingWhiteSpaceCount, lineText.left( bot ) );
292 cursor.insertText( newLine );
293 //cursor.setPosition( cursor.position() + 2 );
294 cursor.movePosition( QTextCursor::StartOfBlock );
295 setTextCursor( cursor );
296 }
297 } else {
298 KRichTextWidget::keyPressEvent( e );
299 }
300 } else {
301 KRichTextWidget::keyPressEvent( e );
302 }
303}
304
305bool TextEdit::isSpellCheckingEnabled() const
306{
307 return d->spellCheckingEnabled;
308}
309
310void TextEdit::setSpellCheckingEnabled( bool enable )
311{
312 EMailQuoteHighlighter *hlighter = dynamic_cast<EMailQuoteHighlighter*>( highlighter() );
313 if ( hlighter ) {
314 hlighter->toggleSpellHighlighting( enable );
315 }
316
317 d->spellCheckingEnabled = enable;
318 emit checkSpellingChanged( enable );
319}
320
321bool TextEdit::shouldBlockBeSpellChecked( const QString &block ) const
322{
323 return !isLineQuoted( block );
324}
325
326bool KPIMTextEdit::TextEdit::isLineQuoted( const QString &line ) const
327{
328 return quoteLength( line ) > 0;
329}
330
331int KPIMTextEdit::TextEdit::quoteLength( const QString &line ) const
332{
333 bool quoteFound = false;
334 int startOfText = -1;
335 const int lineLength( line.length() );
336 for ( int i = 0; i < lineLength; ++i ) {
337 if ( line[i] == QLatin1Char( '>' ) || line[i] == QLatin1Char( '|' ) ) {
338 quoteFound = true;
339 } else if ( line[i] != QLatin1Char( ' ' ) ) {
340 startOfText = i;
341 break;
342 }
343 }
344 if ( quoteFound ) {
345 if ( startOfText == -1 ) {
346 startOfText = line.length() - 1;
347 }
348 return startOfText;
349 } else {
350 return 0;
351 }
352}
353
354const QString KPIMTextEdit::TextEdit::defaultQuoteSign() const
355{
356 return QLatin1String( "> " );
357}
358
359void TextEdit::createHighlighter()
360{
361 EMailQuoteHighlighter *emailHighLighter = new EMailQuoteHighlighter( this );
362
363 setHighlighterColors( emailHighLighter );
364
365 //TODO change config
366 KRichTextWidget::setHighlighter( emailHighLighter );
367
368 if ( !spellCheckingLanguage().isEmpty() ) {
369 setSpellCheckingLanguage( spellCheckingLanguage() );
370 }
371 setSpellCheckingEnabled( isSpellCheckingEnabled() );
372}
373
374void TextEdit::setHighlighterColors( EMailQuoteHighlighter *highlighter )
375{
376 Q_UNUSED( highlighter );
377}
378
379QString TextEdit::toWrappedPlainText() const
380{
381 QTextDocument *doc = document();
382 return toWrappedPlainText( doc );
383}
384
385QString TextEdit::toWrappedPlainText( QTextDocument *doc ) const
386{
387 QString temp;
388 QRegExp rx( QLatin1String( "(http|ftp|ldap)s?\\S+-$" ) );
389 QTextBlock block = doc->begin();
390 while ( block.isValid() ) {
391 QTextLayout *layout = block.layout();
392 const int numberOfLine( layout->lineCount() );
393 bool urlStart = false;
394 for ( int i = 0; i < numberOfLine; ++i ) {
395 QTextLine line = layout->lineAt( i );
396 QString lineText = block.text().mid( line.textStart(), line.textLength() );
397
398 if ( lineText.contains( rx ) ||
399 ( urlStart && !lineText.contains( QLatin1Char( ' ' ) ) &&
400 lineText.endsWith( QLatin1Char( '-' ) ) ) ) {
401 // don't insert line break in URL
402 temp += lineText;
403 urlStart = true;
404 } else {
405 temp += lineText + QLatin1Char( '\n' );
406 }
407 }
408 block = block.next();
409 }
410
411 // Remove the last superfluous newline added above
412 if ( temp.endsWith( QLatin1Char( '\n' ) ) ) {
413 temp.chop( 1 );
414 }
415
416 d->fixupTextEditString( temp );
417 return temp;
418}
419
420QString TextEdit::toCleanPlainText( const QString &plainText ) const
421{
422 QString temp = plainText;
423 d->fixupTextEditString( temp );
424 return temp;
425}
426
427QString TextEdit::toCleanPlainText() const
428{
429 return toCleanPlainText( toPlainText() );
430}
431
432void TextEdit::createActions( KActionCollection *actionCollection )
433{
434 KRichTextWidget::createActions( actionCollection );
435
436 if ( d->imageSupportEnabled ) {
437 d->actionAddImage = new KAction( KIcon( QLatin1String( "insert-image" ) ),
438 i18n( "Add Image" ), this );
439 actionCollection->addAction( QLatin1String( "add_image" ), d->actionAddImage );
440 connect( d->actionAddImage, SIGNAL(triggered(bool)), SLOT(_k_slotAddImage()) );
441 }
442 if ( d->emoticonSupportEnabled ) {
443 d->actionAddEmoticon = new EmoticonTextEditAction( this );
444 actionCollection->addAction( QLatin1String( "add_emoticon" ), d->actionAddEmoticon );
445 connect( d->actionAddEmoticon, SIGNAL(emoticonActivated(QString)),
446 SLOT(_k_slotAddEmoticon(QString)) );
447 }
448
449 if ( d->insertHtmlSupportEnabled ) {
450 d->actionInsertHtml = new KAction( i18n( "Insert HTML" ), this );
451 actionCollection->addAction( QLatin1String( "insert_html" ), d->actionInsertHtml );
452 connect( d->actionInsertHtml, SIGNAL(triggered(bool)), SLOT(_k_slotInsertHtml()) );
453 }
454
455 if ( d->insertTableSupportEnabled ) {
456 d->actionTable = new TableActionMenu( actionCollection, this );
457 d->actionTable->setIcon( KIcon( QLatin1String( "insert-table" ) ) );
458 d->actionTable->setText( i18n( "Table" ) );
459 d->actionTable->setDelayed( false );
460 actionCollection->addAction( QLatin1String( "insert_table" ), d->actionTable );
461 }
462
463 d->actionDeleteLine = new KAction( i18n( "Delete Line" ), this );
464 d->actionDeleteLine->setShortcut( QKeySequence( Qt::CTRL + Qt::Key_K ) );
465 actionCollection->addAction( QLatin1String( "delete_line" ), d->actionDeleteLine );
466 connect( d->actionDeleteLine, SIGNAL(triggered(bool)), SLOT(_k_slotDeleteLine()) );
467
468 d->actionFormatReset =
469 new KAction( KIcon( QLatin1String( "draw-eraser" ) ), i18n( "Reset Font Settings" ), this );
470 d->actionFormatReset->setIconText( i18n( "Reset Font" ) );
471 actionCollection->addAction( QLatin1String( "format_reset" ), d->actionFormatReset );
472 connect( d->actionFormatReset, SIGNAL(triggered(bool)), SLOT(_k_slotFormatReset()) );
473}
474
475void TextEdit::addImage( const KUrl &url, int width, int height )
476{
477 addImageHelper( url, width, height );
478}
479
480void TextEdit::addImage( const KUrl &url )
481{
482 addImageHelper( url );
483}
484
485void TextEdit::addImageHelper( const KUrl &url, int width, int height )
486{
487 QImage image;
488 if ( !image.load( url.path() ) ) {
489 KMessageBox::error(
490 this,
491 i18nc( "@info",
492 "Unable to load image <filename>%1</filename>.",
493 url.path() ) );
494 return;
495 }
496 QFileInfo fi( url.path() );
497 QString imageName =
498 fi.baseName().isEmpty() ?
499 QLatin1String( "image.png" ) :
500 QString( fi.baseName() + QLatin1String( ".png" ) );
501 d->addImageHelper( imageName, image, width, height );
502}
503
504void TextEdit::loadImage ( const QImage &image, const QString &matchName,
505 const QString &resourceName )
506{
507 QSet<int> cursorPositionsToSkip;
508 QTextBlock currentBlock = document()->begin();
509 QTextBlock::iterator it;
510 while ( currentBlock.isValid() ) {
511 for ( it = currentBlock.begin(); !it.atEnd(); ++it ) {
512 QTextFragment fragment = it.fragment();
513 if ( fragment.isValid() ) {
514 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
515 if ( imageFormat.isValid() && imageFormat.name() == matchName ) {
516 int pos = fragment.position();
517 if ( !cursorPositionsToSkip.contains( pos ) ) {
518 QTextCursor cursor( document() );
519 cursor.setPosition( pos );
520 cursor.setPosition( pos + 1, QTextCursor::KeepAnchor );
521 cursor.removeSelectedText();
522 document()->addResource( QTextDocument::ImageResource,
523 QUrl( resourceName ), QVariant( image ) );
524 QTextImageFormat format;
525 format.setName( resourceName );
526 if ( ( imageFormat.width() != 0 ) && ( imageFormat.height() != 0 ) ) {
527 format.setWidth( imageFormat.width() );
528 format.setHeight( imageFormat.height() );
529 }
530 cursor.insertImage( format );
531
532 // The textfragment iterator is now invalid, restart from the beginning
533 // Take care not to replace the same fragment again, or we would be in
534 // an infinite loop.
535 cursorPositionsToSkip.insert( pos );
536 it = currentBlock.begin();
537 }
538 }
539 }
540 }
541 currentBlock = currentBlock.next();
542 }
543}
544
545void TextEditPrivate::addImageHelper( const QString &imageName, const QImage &image,
546 int width, int height )
547{
548 QString imageNameToAdd = imageName;
549 QTextDocument *document = q->document();
550
551 // determine the imageNameToAdd
552 int imageNumber = 1;
553 while ( mImageNames.contains( imageNameToAdd ) ) {
554 QVariant qv = document->resource( QTextDocument::ImageResource, QUrl( imageNameToAdd ) );
555 if ( qv == image ) {
556 // use the same name
557 break;
558 }
559 int firstDot = imageName.indexOf( QLatin1Char( '.' ) );
560 if ( firstDot == -1 ) {
561 imageNameToAdd = imageName + QString::number( imageNumber++ );
562 } else {
563 imageNameToAdd = imageName.left( firstDot ) + QString::number( imageNumber++ ) +
564 imageName.mid( firstDot );
565 }
566 }
567
568 if ( !mImageNames.contains( imageNameToAdd ) ) {
569 document->addResource( QTextDocument::ImageResource, QUrl( imageNameToAdd ), image );
570 mImageNames << imageNameToAdd;
571 }
572 if ( width != -1 && height != -1 ) {
573 QTextImageFormat format;
574 format.setName( imageNameToAdd );
575 format.setWidth( width );
576 format.setHeight( height );
577 q->textCursor().insertImage( format );
578 } else {
579 q->textCursor().insertImage( imageNameToAdd );
580 }
581 q->enableRichTextMode();
582}
583
584ImageWithNameList TextEdit::imagesWithName() const
585{
586 ImageWithNameList retImages;
587 QStringList seenImageNames;
588 QList<QTextImageFormat> imageFormats = d->embeddedImageFormats();
589 foreach ( const QTextImageFormat &imageFormat, imageFormats ) {
590 if ( !seenImageNames.contains( imageFormat.name() ) ) {
591 QVariant resourceData = document()->resource( QTextDocument::ImageResource,
592 QUrl( imageFormat.name() ) );
593 QImage image = qvariant_cast<QImage>( resourceData );
594 QString name = imageFormat.name();
595 ImageWithNamePtr newImage( new ImageWithName );
596 newImage->image = image;
597 newImage->name = name;
598 retImages.append( newImage );
599 seenImageNames.append( imageFormat.name() );
600 }
601 }
602 return retImages;
603}
604
605QList< QSharedPointer<EmbeddedImage> > TextEdit::embeddedImages() const
606{
607 ImageWithNameList normalImages = imagesWithName();
608 QList< QSharedPointer<EmbeddedImage> > retImages;
609 foreach ( const ImageWithNamePtr &normalImage, normalImages ) {
610 QBuffer buffer;
611 buffer.open( QIODevice::WriteOnly );
612 normalImage->image.save( &buffer, "PNG" );
613
614 qsrand( QDateTime::currentDateTime().toTime_t() + qHash( normalImage->name ) );
615 QSharedPointer<EmbeddedImage> embeddedImage( new EmbeddedImage() );
616 retImages.append( embeddedImage );
617 embeddedImage->image = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() );
618 embeddedImage->imageName = normalImage->name;
619 embeddedImage->contentID = QString( QLatin1String( "%1@KDE" ) ).arg( qrand() );
620 }
621 return retImages;
622}
623
624QList<QTextImageFormat> TextEditPrivate::embeddedImageFormats() const
625{
626 QTextDocument *doc = q->document();
627 QList<QTextImageFormat> retList;
628
629 QTextBlock currentBlock = doc->begin();
630 while ( currentBlock.isValid() ) {
631 QTextBlock::iterator it;
632 for ( it = currentBlock.begin(); !it.atEnd(); ++it ) {
633 QTextFragment fragment = it.fragment();
634 if ( fragment.isValid() ) {
635 QTextImageFormat imageFormat = fragment.charFormat().toImageFormat();
636 if ( imageFormat.isValid() ) {
637 //TODO: Replace with a way to see if an image is an embedded image or a remote
638 QUrl url( imageFormat.name() );
639 if ( !url.isValid() || !url.scheme().startsWith( QLatin1String( "http" ) ) ) {
640 retList.append( imageFormat );
641 }
642 }
643 }
644 }
645 currentBlock = currentBlock.next();
646 }
647 return retList;
648}
649
650void TextEditPrivate::_k_slotAddEmoticon( const QString &text )
651{
652 QTextCursor cursor = q->textCursor();
653 cursor.insertText( text );
654}
655
656void TextEditPrivate::_k_slotInsertHtml()
657{
658 if ( q->textMode() == KRichTextEdit::Rich ) {
659 QPointer<InsertHtmlDialog> dialog = new InsertHtmlDialog( q );
660 if ( dialog->exec() ) {
661 const QString str = dialog->html();
662 if ( !str.isEmpty() ) {
663 QTextCursor cursor = q->textCursor();
664 cursor.insertHtml( str );
665 }
666 }
667 delete dialog;
668 }
669}
670
671void TextEditPrivate::_k_slotAddImage()
672{
673 QPointer<InsertImageDialog> dlg = new InsertImageDialog( q );
674 if ( dlg->exec() == KDialog::Accepted && dlg ) {
675 const KUrl url = dlg->imageUrl();
676 int imageWidth = -1;
677 int imageHeight = -1;
678 if ( !dlg->keepOriginalSize() ) {
679 imageWidth = dlg->imageWidth();
680 imageHeight = dlg->imageHeight();
681 }
682 q->addImage( url, imageWidth, imageHeight );
683 }
684 delete dlg;
685}
686
687void TextEditPrivate::_k_slotTextModeChanged( KRichTextEdit::Mode mode )
688{
689 if ( mode == KRichTextEdit::Rich ) {
690 saveFont = q->currentFont();
691 }
692}
693
694void TextEditPrivate::_k_slotFormatReset()
695{
696 q->setTextBackgroundColor( q->palette().highlightedText().color() );
697 q->setTextForegroundColor( q->palette().text().color() );
698 q->setFont( saveFont );
699
700}
701
702void KPIMTextEdit::TextEdit::enableImageActions()
703{
704 d->imageSupportEnabled = true;
705}
706
707bool KPIMTextEdit::TextEdit::isEnableImageActions() const
708{
709 return d->imageSupportEnabled;
710}
711
712void KPIMTextEdit::TextEdit::enableEmoticonActions()
713{
714 d->emoticonSupportEnabled = true;
715}
716
717bool KPIMTextEdit::TextEdit::isEnableEmoticonActions() const
718{
719 return d->emoticonSupportEnabled;
720}
721
722void KPIMTextEdit::TextEdit::enableInsertHtmlActions()
723{
724 d->insertHtmlSupportEnabled = true;
725}
726
727bool KPIMTextEdit::TextEdit::isEnableInsertHtmlActions() const
728{
729 return d->insertHtmlSupportEnabled;
730}
731
732bool KPIMTextEdit::TextEdit::isEnableInsertTableActions() const
733{
734 return d->insertTableSupportEnabled;
735}
736
737void KPIMTextEdit::TextEdit::enableInsertTableActions()
738{
739 d->insertTableSupportEnabled = true;
740}
741
742QByteArray KPIMTextEdit::TextEdit::imageNamesToContentIds(
743 const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList )
744{
745 QByteArray result = htmlBody;
746 if ( !imageList.isEmpty() ) {
747 foreach ( const QSharedPointer<EmbeddedImage> &image, imageList ) {
748 const QString newImageName = QLatin1String( "cid:" ) + image->contentID;
749 QByteArray quote( "\"" );
750 result.replace( QByteArray( quote + image->imageName.toLocal8Bit() + quote ),
751 QByteArray( quote + newImageName.toLocal8Bit() + quote ) );
752 }
753 }
754 return result;
755}
756
757void TextEdit::insertImage( const QImage &image, const QFileInfo &fileInfo )
758{
759 QString imageName = fileInfo.baseName().isEmpty() ?
760 i18nc( "Start of the filename for an image", "image" ) :
761 fileInfo.baseName();
762 d->addImageHelper( imageName, image );
763}
764
765void TextEdit::insertFromMimeData( const QMimeData *source )
766{
767 // Add an image if that is on the clipboard
768 if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) {
769 QImage image = qvariant_cast<QImage>( source->imageData() );
770 QFileInfo fi;
771 insertImage( image, fi );
772 return;
773 }
774
775 // Attempt to paste HTML contents into the text edit in plain text mode,
776 // prevent this and prevent plain text instead.
777 if ( textMode() == KRichTextEdit::Plain && source->hasHtml() ) {
778 if ( source->hasText() ) {
779 insertPlainText( source->text() );
780 return;
781 }
782 }
783
784 KRichTextWidget::insertFromMimeData( source );
785}
786
787bool KPIMTextEdit::TextEdit::canInsertFromMimeData( const QMimeData *source ) const
788{
789 if ( source->hasHtml() && textMode() == KRichTextEdit::Rich ) {
790 return true;
791 }
792
793 if ( source->hasText() ) {
794 return true;
795 }
796
797 if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) {
798 return true;
799 }
800
801 return KRichTextWidget::canInsertFromMimeData( source );
802}
803
804bool TextEdit::isFormattingUsed() const
805{
806 if ( textMode() == Plain ) {
807 return false;
808 }
809
810 return TextUtils::containsFormatting( document() );
811}
812
813void TextEditPrivate::_k_slotDeleteLine()
814{
815 if ( q->hasFocus() ) {
816 q->deleteCurrentLine();
817 }
818}
819
820void TextEdit::deleteCurrentLine()
821{
822 QTextCursor cursor = textCursor();
823 QTextBlock block = cursor.block();
824 const QTextLayout *layout = block.layout();
825
826 // The current text block can have several lines due to word wrapping.
827 // Search the line the cursor is in, and then delete it.
828 for ( int lineNumber = 0; lineNumber < layout->lineCount(); lineNumber++ ) {
829 QTextLine line = layout->lineAt( lineNumber );
830 const bool lastLineInBlock = ( line.textStart() + line.textLength() == block.length() - 1 );
831 const bool oneLineBlock = ( layout->lineCount() == 1 );
832 const int startOfLine = block.position() + line.textStart();
833 int endOfLine = block.position() + line.textStart() + line.textLength();
834 if ( !lastLineInBlock ) {
835 endOfLine -= 1;
836 }
837
838 // Found the line where the cursor is in
839 if ( cursor.position() >= startOfLine && cursor.position() <= endOfLine ) {
840 int deleteStart = startOfLine;
841 int deleteLength = line.textLength();
842 if ( oneLineBlock ) {
843 deleteLength++; // The trailing newline
844 }
845
846 // When deleting the last line in the document,
847 // remove the newline of the line before the last line instead
848 if ( deleteStart + deleteLength >= document()->characterCount() &&
849 deleteStart > 0 ) {
850 deleteStart--;
851 }
852
853 cursor.beginEditBlock();
854 cursor.setPosition( deleteStart );
855 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength );
856 cursor.removeSelectedText();
857 cursor.endEditBlock();
858 return;
859 }
860 }
861}
862
863#include "moc_textedit.cpp"
864