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 | |
52 | namespace KPIMTextEdit { |
53 | |
54 | class 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 | |
168 | using namespace KPIMTextEdit; |
169 | |
170 | void 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 | |
183 | TextEdit::TextEdit( const QString &text, QWidget *parent ) |
184 | : KRichTextWidget( text, parent ), |
185 | d( new TextEditPrivate( this ) ) |
186 | { |
187 | d->init(); |
188 | } |
189 | |
190 | TextEdit::TextEdit( QWidget *parent ) |
191 | : KRichTextWidget( parent ), |
192 | d( new TextEditPrivate( this ) ) |
193 | { |
194 | d->init(); |
195 | } |
196 | |
197 | TextEdit::TextEdit( QWidget *parent, const QString &configFile ) |
198 | : KRichTextWidget( parent ), |
199 | d( new TextEditPrivate( this ) ) |
200 | { |
201 | d->init(); |
202 | d->configFile = configFile; |
203 | } |
204 | |
205 | TextEdit::~TextEdit() |
206 | { |
207 | } |
208 | |
209 | bool 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 | |
219 | void 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 | |
240 | QString TextEdit::configFile() const |
241 | { |
242 | return d->configFile; |
243 | } |
244 | |
245 | void 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 | |
305 | bool TextEdit::isSpellCheckingEnabled() const |
306 | { |
307 | return d->spellCheckingEnabled; |
308 | } |
309 | |
310 | void 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 | |
321 | bool TextEdit::shouldBlockBeSpellChecked( const QString &block ) const |
322 | { |
323 | return !isLineQuoted( block ); |
324 | } |
325 | |
326 | bool KPIMTextEdit::TextEdit::isLineQuoted( const QString &line ) const |
327 | { |
328 | return quoteLength( line ) > 0; |
329 | } |
330 | |
331 | int 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 | |
354 | const QString KPIMTextEdit::TextEdit::defaultQuoteSign() const |
355 | { |
356 | return QLatin1String( "> " ); |
357 | } |
358 | |
359 | void 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 | |
374 | void TextEdit::setHighlighterColors( EMailQuoteHighlighter *highlighter ) |
375 | { |
376 | Q_UNUSED( highlighter ); |
377 | } |
378 | |
379 | QString TextEdit::toWrappedPlainText() const |
380 | { |
381 | QTextDocument *doc = document(); |
382 | return toWrappedPlainText( doc ); |
383 | } |
384 | |
385 | QString 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 | |
420 | QString TextEdit::toCleanPlainText( const QString &plainText ) const |
421 | { |
422 | QString temp = plainText; |
423 | d->fixupTextEditString( temp ); |
424 | return temp; |
425 | } |
426 | |
427 | QString TextEdit::toCleanPlainText() const |
428 | { |
429 | return toCleanPlainText( toPlainText() ); |
430 | } |
431 | |
432 | void 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 | |
475 | void TextEdit::addImage( const KUrl &url, int width, int height ) |
476 | { |
477 | addImageHelper( url, width, height ); |
478 | } |
479 | |
480 | void TextEdit::addImage( const KUrl &url ) |
481 | { |
482 | addImageHelper( url ); |
483 | } |
484 | |
485 | void 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 | |
504 | void 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 | |
545 | void 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 | |
584 | ImageWithNameList 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 | |
605 | QList< 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 | |
624 | QList<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 | |
650 | void TextEditPrivate::_k_slotAddEmoticon( const QString &text ) |
651 | { |
652 | QTextCursor cursor = q->textCursor(); |
653 | cursor.insertText( text ); |
654 | } |
655 | |
656 | void 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 | |
671 | void 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 | |
687 | void TextEditPrivate::_k_slotTextModeChanged( KRichTextEdit::Mode mode ) |
688 | { |
689 | if ( mode == KRichTextEdit::Rich ) { |
690 | saveFont = q->currentFont(); |
691 | } |
692 | } |
693 | |
694 | void 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 | |
702 | void KPIMTextEdit::TextEdit::enableImageActions() |
703 | { |
704 | d->imageSupportEnabled = true; |
705 | } |
706 | |
707 | bool KPIMTextEdit::TextEdit::isEnableImageActions() const |
708 | { |
709 | return d->imageSupportEnabled; |
710 | } |
711 | |
712 | void KPIMTextEdit::TextEdit::enableEmoticonActions() |
713 | { |
714 | d->emoticonSupportEnabled = true; |
715 | } |
716 | |
717 | bool KPIMTextEdit::TextEdit::isEnableEmoticonActions() const |
718 | { |
719 | return d->emoticonSupportEnabled; |
720 | } |
721 | |
722 | void KPIMTextEdit::TextEdit::enableInsertHtmlActions() |
723 | { |
724 | d->insertHtmlSupportEnabled = true; |
725 | } |
726 | |
727 | bool KPIMTextEdit::TextEdit::isEnableInsertHtmlActions() const |
728 | { |
729 | return d->insertHtmlSupportEnabled; |
730 | } |
731 | |
732 | bool KPIMTextEdit::TextEdit::isEnableInsertTableActions() const |
733 | { |
734 | return d->insertTableSupportEnabled; |
735 | } |
736 | |
737 | void KPIMTextEdit::TextEdit::enableInsertTableActions() |
738 | { |
739 | d->insertTableSupportEnabled = true; |
740 | } |
741 | |
742 | QByteArray 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 | |
757 | void 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 | |
765 | void 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 | |
787 | bool 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 | |
804 | bool TextEdit::isFormattingUsed() const |
805 | { |
806 | if ( textMode() == Plain ) { |
807 | return false; |
808 | } |
809 | |
810 | return TextUtils::containsFormatting( document() ); |
811 | } |
812 | |
813 | void TextEditPrivate::_k_slotDeleteLine() |
814 | { |
815 | if ( q->hasFocus() ) { |
816 | q->deleteCurrentLine(); |
817 | } |
818 | } |
819 | |
820 | void 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 | |