1 /*
2    SPDX-FileCopyrightText: 2015-2021 Laurent Montel <montel@kde.org>
3 
4    SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "richtextcomposercontroler.h"
8 #include "inserthtmldialog.h"
9 #include "klinkdialog_p.h"
10 #include "nestedlisthelper_p.h"
11 #include "richtextcomposerimages.h"
12 #include <QApplication>
13 #include <QRegularExpression>
14 
15 #include "insertimagedialog.h"
16 #include "textutils.h"
17 #include <KColorScheme>
18 #include <KLocalizedString>
19 #include <KMessageBox>
20 #include <QClipboard>
21 #include <QColorDialog>
22 #include <QIcon>
23 #include <QPointer>
24 #include <QTextBlock>
25 #include <QTextDocumentFragment>
26 #include <QTextList>
27 #include <QTimer>
28 #include <chrono>
29 
30 using namespace std::chrono_literals;
31 
32 using namespace KPIMTextEdit;
33 
34 class Q_DECL_HIDDEN RichTextComposerControler::RichTextComposerControlerPrivate
35 {
36 public:
RichTextComposerControlerPrivate(RichTextComposer * composer,RichTextComposerControler * qq)37     RichTextComposerControlerPrivate(RichTextComposer *composer, RichTextComposerControler *qq)
38         : richtextComposer(composer)
39         , q(qq)
40     {
41         nestedListHelper = new NestedListHelper(composer);
42         richTextImages = new RichTextComposerImages(richtextComposer, q);
43     }
44 
~RichTextComposerControlerPrivate()45     ~RichTextComposerControlerPrivate()
46     {
47         delete nestedListHelper;
48     }
49 
linkColor()50     QColor linkColor()
51     {
52         if (mLinkColor.isValid()) {
53             return mLinkColor;
54         }
55         mLinkColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color();
56         return mLinkColor;
57     }
58 
59     void selectLinkText(QTextCursor *cursor) const;
60     void fixupTextEditString(QString &text) const;
61     void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
62     Q_REQUIRED_RESULT QString addQuotesToText(const QString &inputText, const QString &defaultQuoteSign);
63     void updateLink(const QString &linkUrl, const QString &linkText);
64     QFont saveFont;
65     QColor mLinkColor;
66     QTextCharFormat painterFormat;
67     NestedListHelper *nestedListHelper = nullptr;
68     RichTextComposer *richtextComposer = nullptr;
69     RichTextComposerImages *richTextImages = nullptr;
70     RichTextComposerControler *const q;
71     bool painterActive = false;
72 };
73 
selectLinkText(QTextCursor * cursor) const74 void RichTextComposerControler::RichTextComposerControlerPrivate::selectLinkText(QTextCursor *cursor) const
75 {
76     // If the cursor is on a link, select the text of the link.
77     if (cursor->charFormat().isAnchor()) {
78         const QString aHref = cursor->charFormat().anchorHref();
79 
80         // Move cursor to start of link
81         while (cursor->charFormat().anchorHref() == aHref) {
82             if (cursor->atStart()) {
83                 break;
84             }
85             cursor->setPosition(cursor->position() - 1);
86         }
87         if (cursor->charFormat().anchorHref() != aHref) {
88             cursor->setPosition(cursor->position() + 1, QTextCursor::KeepAnchor);
89         }
90 
91         // Move selection to the end of the link
92         while (cursor->charFormat().anchorHref() == aHref) {
93             if (cursor->atEnd()) {
94                 break;
95             }
96             const int oldPosition = cursor->position();
97             cursor->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
98             // Wordaround Qt Bug. when we have a table.
99             // FIXME selection url
100             if (oldPosition == cursor->position()) {
101                 break;
102             }
103         }
104         if (cursor->charFormat().anchorHref() != aHref) {
105             cursor->setPosition(cursor->position() - 1, QTextCursor::KeepAnchor);
106         }
107     } else if (cursor->hasSelection()) {
108         // Nothing to do. Using the currently selected text as the link text.
109     } else {
110         // Select current word
111         cursor->movePosition(QTextCursor::StartOfWord);
112         cursor->movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
113     }
114 }
115 
mergeFormatOnWordOrSelection(const QTextCharFormat & format)116 void RichTextComposerControler::RichTextComposerControlerPrivate::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
117 {
118     QTextCursor cursor = richtextComposer->textCursor();
119     QTextCursor wordStart(cursor);
120     QTextCursor wordEnd(cursor);
121 
122     wordStart.movePosition(QTextCursor::StartOfWord);
123     wordEnd.movePosition(QTextCursor::EndOfWord);
124 
125     cursor.beginEditBlock();
126     if (!cursor.hasSelection() && cursor.position() != wordStart.position() && cursor.position() != wordEnd.position()) {
127         cursor.select(QTextCursor::WordUnderCursor);
128     }
129     cursor.mergeCharFormat(format);
130     richtextComposer->mergeCurrentCharFormat(format);
131     cursor.endEditBlock();
132 }
133 
RichTextComposerControler(RichTextComposer * richtextComposer,QObject * parent)134 RichTextComposerControler::RichTextComposerControler(RichTextComposer *richtextComposer, QObject *parent)
135     : QObject(parent)
136     , d(new RichTextComposerControlerPrivate(richtextComposer, this))
137 {
138 }
139 
140 RichTextComposerControler::~RichTextComposerControler() = default;
141 
painterActive() const142 bool RichTextComposerControler::painterActive() const
143 {
144     return d->painterActive;
145 }
146 
addCheckbox(bool add)147 void RichTextComposerControler::addCheckbox(bool add)
148 {
149     QTextBlockFormat fmt;
150     fmt.setMarker(add ? QTextBlockFormat::MarkerType::Unchecked : QTextBlockFormat::MarkerType::NoMarker);
151     QTextCursor cursor = richTextComposer()->textCursor();
152     cursor.beginEditBlock();
153     if (add && !cursor.currentList()) {
154         // Checkbox only works with lists, so if we are not at list, add a new one
155         setListStyle(1);
156     } else if (!add && cursor.currentList() && cursor.currentList()->count() == 1) {
157         // If this is a single-element list with a checkbox, and user disables
158         // a checkbox, assume user don't want a list too
159         // (so when cursor is not on a list, and enables checkbox and disables
160         // it right after, he returns to the same state with no list)
161         setListStyle(0);
162     }
163     cursor.mergeBlockFormat(fmt);
164     cursor.endEditBlock();
165 }
166 
setFontForWholeText(const QFont & font)167 void RichTextComposerControler::setFontForWholeText(const QFont &font)
168 {
169     QTextCharFormat fmt;
170     fmt.setFont(font);
171     QTextCursor cursor(richTextComposer()->document());
172     cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
173     cursor.mergeCharFormat(fmt);
174     richTextComposer()->document()->setDefaultFont(font);
175 }
176 
disablePainter()177 void RichTextComposerControler::disablePainter()
178 {
179     // If the painter is active, paint the selection with the
180     // correct format.
181     if (richTextComposer()->textCursor().hasSelection()) {
182         QTextCursor cursor = richTextComposer()->textCursor();
183         cursor.setCharFormat(d->painterFormat);
184         richTextComposer()->setTextCursor(cursor);
185     }
186     d->painterActive = false;
187 }
188 
composerImages() const189 RichTextComposerImages *RichTextComposerControler::composerImages() const
190 {
191     return d->richTextImages;
192 }
193 
nestedListHelper() const194 NestedListHelper *RichTextComposerControler::nestedListHelper() const
195 {
196     return d->nestedListHelper;
197 }
198 
ensureCursorVisibleDelayed()199 void RichTextComposerControler::ensureCursorVisibleDelayed()
200 {
201     d->richtextComposer->ensureCursorVisible();
202 }
203 
richTextComposer() const204 RichTextComposer *RichTextComposerControler::richTextComposer() const
205 {
206     return d->richtextComposer;
207 }
208 
insertHorizontalRule()209 void RichTextComposerControler::insertHorizontalRule()
210 {
211     QTextCursor cursor = richTextComposer()->textCursor();
212     QTextBlockFormat bf = cursor.blockFormat();
213     QTextCharFormat cf = cursor.charFormat();
214 
215     cursor.beginEditBlock();
216     cursor.insertHtml(QStringLiteral("<hr>"));
217     cursor.insertBlock(bf, cf);
218     cursor.endEditBlock();
219     richTextComposer()->setTextCursor(cursor);
220     richTextComposer()->activateRichText();
221 }
222 
alignLeft()223 void RichTextComposerControler::alignLeft()
224 {
225     richTextComposer()->setAlignment(Qt::AlignLeft);
226     richTextComposer()->setFocus();
227     richTextComposer()->activateRichText();
228 }
229 
alignCenter()230 void RichTextComposerControler::alignCenter()
231 {
232     richTextComposer()->setAlignment(Qt::AlignHCenter);
233     richTextComposer()->setFocus();
234     richTextComposer()->activateRichText();
235 }
236 
alignRight()237 void RichTextComposerControler::alignRight()
238 {
239     richTextComposer()->setAlignment(Qt::AlignRight);
240     richTextComposer()->setFocus();
241     richTextComposer()->activateRichText();
242 }
243 
alignJustify()244 void RichTextComposerControler::alignJustify()
245 {
246     richTextComposer()->setAlignment(Qt::AlignJustify);
247     richTextComposer()->setFocus();
248     richTextComposer()->activateRichText();
249 }
250 
makeRightToLeft()251 void RichTextComposerControler::makeRightToLeft()
252 {
253     QTextBlockFormat format;
254     format.setLayoutDirection(Qt::RightToLeft);
255     QTextCursor cursor = richTextComposer()->textCursor();
256     cursor.mergeBlockFormat(format);
257     richTextComposer()->setTextCursor(cursor);
258     richTextComposer()->setFocus();
259     richTextComposer()->activateRichText();
260 }
261 
makeLeftToRight()262 void RichTextComposerControler::makeLeftToRight()
263 {
264     QTextBlockFormat format;
265     format.setLayoutDirection(Qt::LeftToRight);
266     QTextCursor cursor = richTextComposer()->textCursor();
267     cursor.mergeBlockFormat(format);
268     richTextComposer()->setTextCursor(cursor);
269     richTextComposer()->setFocus();
270     richTextComposer()->activateRichText();
271 }
272 
setTextBold(bool bold)273 void RichTextComposerControler::setTextBold(bool bold)
274 {
275     QTextCharFormat fmt;
276     fmt.setFontWeight(bold ? QFont::Bold : QFont::Normal);
277     d->mergeFormatOnWordOrSelection(fmt);
278     richTextComposer()->setFocus();
279     richTextComposer()->activateRichText();
280 }
281 
setTextItalic(bool italic)282 void RichTextComposerControler::setTextItalic(bool italic)
283 {
284     QTextCharFormat fmt;
285     fmt.setFontItalic(italic);
286     d->mergeFormatOnWordOrSelection(fmt);
287     richTextComposer()->setFocus();
288     richTextComposer()->activateRichText();
289 }
290 
setTextUnderline(bool underline)291 void RichTextComposerControler::setTextUnderline(bool underline)
292 {
293     QTextCharFormat fmt;
294     fmt.setFontUnderline(underline);
295     d->mergeFormatOnWordOrSelection(fmt);
296     richTextComposer()->setFocus();
297     richTextComposer()->activateRichText();
298 }
299 
setTextStrikeOut(bool strikeOut)300 void RichTextComposerControler::setTextStrikeOut(bool strikeOut)
301 {
302     QTextCharFormat fmt;
303     fmt.setFontStrikeOut(strikeOut);
304     d->mergeFormatOnWordOrSelection(fmt);
305     richTextComposer()->setFocus();
306     richTextComposer()->activateRichText();
307 }
308 
setTextForegroundColor(const QColor & color)309 void RichTextComposerControler::setTextForegroundColor(const QColor &color)
310 {
311     QTextCharFormat fmt;
312     fmt.setForeground(color);
313     d->mergeFormatOnWordOrSelection(fmt);
314     richTextComposer()->setFocus();
315     richTextComposer()->activateRichText();
316 }
317 
setTextBackgroundColor(const QColor & color)318 void RichTextComposerControler::setTextBackgroundColor(const QColor &color)
319 {
320     QTextCharFormat fmt;
321     fmt.setBackground(color);
322     d->mergeFormatOnWordOrSelection(fmt);
323     richTextComposer()->setFocus();
324     richTextComposer()->activateRichText();
325 }
326 
setFontFamily(const QString & fontFamily)327 void RichTextComposerControler::setFontFamily(const QString &fontFamily)
328 {
329     QTextCharFormat fmt;
330     fmt.setFontFamily(fontFamily);
331     d->mergeFormatOnWordOrSelection(fmt);
332     richTextComposer()->setFocus();
333     richTextComposer()->activateRichText();
334 }
335 
setFontSize(int size)336 void RichTextComposerControler::setFontSize(int size)
337 {
338     QTextCharFormat fmt;
339     fmt.setFontPointSize(size);
340     d->mergeFormatOnWordOrSelection(fmt);
341     richTextComposer()->setFocus();
342     richTextComposer()->activateRichText();
343 }
344 
setFont(const QFont & font)345 void RichTextComposerControler::setFont(const QFont &font)
346 {
347     QTextCharFormat fmt;
348     fmt.setFont(font);
349     d->mergeFormatOnWordOrSelection(fmt);
350     richTextComposer()->setFocus();
351     richTextComposer()->activateRichText();
352 }
353 
setTextSuperScript(bool superscript)354 void RichTextComposerControler::setTextSuperScript(bool superscript)
355 {
356     QTextCharFormat fmt;
357     fmt.setVerticalAlignment(superscript ? QTextCharFormat::AlignSuperScript : QTextCharFormat::AlignNormal);
358     d->mergeFormatOnWordOrSelection(fmt);
359     richTextComposer()->setFocus();
360     richTextComposer()->activateRichText();
361 }
362 
setTextSubScript(bool subscript)363 void RichTextComposerControler::setTextSubScript(bool subscript)
364 {
365     QTextCharFormat fmt;
366     fmt.setVerticalAlignment(subscript ? QTextCharFormat::AlignSubScript : QTextCharFormat::AlignNormal);
367     d->mergeFormatOnWordOrSelection(fmt);
368     richTextComposer()->setFocus();
369     richTextComposer()->activateRichText();
370 }
371 
setHeadingLevel(int level)372 void RichTextComposerControler::setHeadingLevel(int level)
373 {
374     const int boundedLevel = qBound(0, 6, level);
375     // Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
376     // level=2 look the same
377     const int sizeAdjustment = boundedLevel > 0 ? 5 - boundedLevel : 0;
378 
379     QTextCursor cursor = richTextComposer()->textCursor();
380     cursor.beginEditBlock();
381 
382     QTextBlockFormat blkfmt;
383     blkfmt.setHeadingLevel(boundedLevel);
384     cursor.mergeBlockFormat(blkfmt);
385 
386     QTextCharFormat chrfmt;
387     chrfmt.setFontWeight(boundedLevel > 0 ? QFont::Bold : QFont::Normal);
388     chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
389     // Applying style to the current line or selection
390     QTextCursor selectCursor = cursor;
391     if (selectCursor.hasSelection()) {
392         QTextCursor top = selectCursor;
393         top.setPosition(qMin(top.anchor(), top.position()));
394         top.movePosition(QTextCursor::StartOfBlock);
395 
396         QTextCursor bottom = selectCursor;
397         bottom.setPosition(qMax(bottom.anchor(), bottom.position()));
398         bottom.movePosition(QTextCursor::EndOfBlock);
399 
400         selectCursor.setPosition(top.position(), QTextCursor::MoveAnchor);
401         selectCursor.setPosition(bottom.position(), QTextCursor::KeepAnchor);
402     } else {
403         selectCursor.select(QTextCursor::BlockUnderCursor);
404     }
405     selectCursor.mergeCharFormat(chrfmt);
406 
407     cursor.mergeBlockCharFormat(chrfmt);
408     cursor.endEditBlock();
409     richTextComposer()->setTextCursor(cursor);
410     richTextComposer()->setFocus();
411     richTextComposer()->activateRichText();
412 }
413 
setChangeTextForegroundColor()414 void RichTextComposerControler::setChangeTextForegroundColor()
415 {
416     const QColor currentColor = richTextComposer()->textColor();
417     const QColor defaultColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground().color();
418 
419     const QColor selectedColor = QColorDialog::getColor(currentColor.isValid() ? currentColor : defaultColor, richTextComposer());
420 
421     if (!selectedColor.isValid() && !currentColor.isValid()) {
422         setTextForegroundColor(defaultColor);
423     } else if (selectedColor.isValid()) {
424         setTextForegroundColor(selectedColor);
425     }
426 }
427 
setChangeTextBackgroundColor()428 void RichTextComposerControler::setChangeTextBackgroundColor()
429 {
430     QTextCharFormat fmt = richTextComposer()->textCursor().charFormat();
431     const QColor currentColor = fmt.background().color();
432     const QColor defaultColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground().color();
433 
434     const QColor selectedColor = QColorDialog::getColor(currentColor.isValid() ? currentColor : defaultColor, richTextComposer());
435 
436     if (!selectedColor.isValid() && !currentColor.isValid()) {
437         setTextBackgroundColor(defaultColor);
438     } else if (selectedColor.isValid()) {
439         setTextBackgroundColor(selectedColor);
440     }
441 }
442 
currentLinkUrl() const443 QString RichTextComposerControler::currentLinkUrl() const
444 {
445     return richTextComposer()->textCursor().charFormat().anchorHref();
446 }
447 
currentLinkText() const448 QString RichTextComposerControler::currentLinkText() const
449 {
450     QTextCursor cursor = richTextComposer()->textCursor();
451     d->selectLinkText(&cursor);
452     return cursor.selectedText();
453 }
454 
selectLinkText() const455 void RichTextComposerControler::selectLinkText() const
456 {
457     QTextCursor cursor = richTextComposer()->textCursor();
458     d->selectLinkText(&cursor);
459     richTextComposer()->setTextCursor(cursor);
460 }
461 
manageLink()462 void RichTextComposerControler::manageLink()
463 {
464     selectLinkText();
465     QPointer<KLinkDialog> linkDialog = new KLinkDialog(richTextComposer());
466     linkDialog->setLinkText(currentLinkText());
467     linkDialog->setLinkUrl(currentLinkUrl());
468 
469     if (linkDialog->exec()) {
470         d->updateLink(linkDialog->linkUrl(), linkDialog->linkText());
471     }
472 
473     delete linkDialog;
474 }
475 
updateLink(const QString & linkUrl,const QString & linkText)476 void RichTextComposerControler::updateLink(const QString &linkUrl, const QString &linkText)
477 {
478     d->updateLink(linkUrl, linkText);
479 }
480 
updateLink(const QString & linkUrl,const QString & linkText)481 void RichTextComposerControler::RichTextComposerControlerPrivate::updateLink(const QString &linkUrl, const QString &linkText)
482 {
483     q->selectLinkText();
484 
485     QTextCursor cursor = richtextComposer->textCursor();
486     cursor.beginEditBlock();
487 
488     if (!cursor.hasSelection()) {
489         cursor.select(QTextCursor::WordUnderCursor);
490     }
491 
492     QTextCharFormat format = cursor.charFormat();
493     // Save original format to create an extra space with the existing char
494     // format for the block
495     if (!linkUrl.isEmpty()) {
496         // Add link details
497         format.setAnchor(true);
498         format.setAnchorHref(linkUrl);
499         // Workaround for QTBUG-1814:
500         // Link formatting does not get applied immediately when setAnchor(true)
501         // is called.  So the formatting needs to be applied manually.
502         format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
503         format.setUnderlineColor(linkColor());
504         format.setForeground(linkColor());
505         richtextComposer->activateRichText();
506     } else {
507         // Remove link details
508         format.setAnchor(false);
509         format.setAnchorHref(QString());
510         // Workaround for QTBUG-1814:
511         // Link formatting does not get removed immediately when setAnchor(false)
512         // is called. So the formatting needs to be applied manually.
513         QTextDocument defaultTextDocument;
514         QTextCharFormat defaultCharFormat = defaultTextDocument.begin().charFormat();
515 
516         format.setUnderlineStyle(defaultCharFormat.underlineStyle());
517         format.setUnderlineColor(defaultCharFormat.underlineColor());
518         format.setForeground(defaultCharFormat.foreground());
519     }
520 
521     // Insert link text specified in dialog, otherwise write out url.
522     QString _linkText;
523     if (!linkText.isEmpty()) {
524         _linkText = linkText;
525     } else {
526         _linkText = linkUrl;
527     }
528     cursor.insertText(_linkText, format);
529 
530     cursor.endEditBlock();
531 }
532 
toCleanHtml() const533 QString RichTextComposerControler::toCleanHtml() const
534 {
535     QString result = richTextComposer()->toHtml();
536 
537     static const QString EMPTYLINEHTML = QStringLiteral(
538         "<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; "
539         "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; \">&nbsp;</p>");
540 
541     // Qt inserts various style properties based on the current mode of the editor (underline,
542     // bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'.
543     static const QString EMPTYLINEREGEX = QStringLiteral("<p style=\"-qt-paragraph-type:empty;(.*)</p>");
544 
545     static const QString OLLISTPATTERNQT = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
546 
547     static const QString ULLISTPATTERNQT = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
548 
549     static const QString ORDEREDLISTHTML = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px;");
550 
551     static const QString UNORDEREDLISTHTML = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px;");
552 
553     // fix 1 - empty lines should show as empty lines - MS Outlook treats margin-top:0px; as
554     // a non-existing line.
555     // Although we can simply remove the margin-top style property, we still get unwanted results
556     // if you have three or more empty lines. It's best to replace empty <p> elements with <p>&nbsp;</p>.
557 
558     QRegExp emptyLineFinder(EMPTYLINEREGEX);
559     emptyLineFinder.setMinimal(true);
560 
561     // find the first occurrence
562     int offset = emptyLineFinder.indexIn(result, 0);
563     while (offset != -1) {
564         // replace all the matching text with the new line text
565         result.replace(offset, emptyLineFinder.matchedLength(), EMPTYLINEHTML);
566         // advance the search offset to just beyond the last replace
567         offset += EMPTYLINEHTML.length();
568         // find the next occurrence
569         offset = emptyLineFinder.indexIn(result, offset);
570     }
571 
572     // fix 2a - ordered lists - MS Outlook treats margin-left:0px; as
573     // a non-existing number; e.g: "1. First item" turns into "First Item"
574     result.replace(OLLISTPATTERNQT, ORDEREDLISTHTML);
575 
576     // fix 2b - unordered lists - MS Outlook treats margin-left:0px; as
577     // a non-existing bullet; e.g: "* First bullet" turns into "First Bullet"
578     result.replace(ULLISTPATTERNQT, UNORDEREDLISTHTML);
579 
580     return result;
581 }
582 
canIndentList() const583 bool RichTextComposerControler::canIndentList() const
584 {
585     return d->nestedListHelper->canIndent();
586 }
587 
canDedentList() const588 bool RichTextComposerControler::canDedentList() const
589 {
590     return d->nestedListHelper->canDedent();
591 }
592 
indentListMore()593 void RichTextComposerControler::indentListMore()
594 {
595     d->nestedListHelper->handleOnIndentMore();
596     richTextComposer()->activateRichText();
597 }
598 
indentListLess()599 void RichTextComposerControler::indentListLess()
600 {
601     d->nestedListHelper->handleOnIndentLess();
602 }
603 
setListStyle(int styleIndex)604 void RichTextComposerControler::setListStyle(int styleIndex)
605 {
606     d->nestedListHelper->handleOnBulletType(-styleIndex);
607     richTextComposer()->setFocus();
608     richTextComposer()->activateRichText();
609 }
610 
insertLink(const QString & url)611 void RichTextComposerControler::insertLink(const QString &url)
612 {
613     if (url.isEmpty()) {
614         return;
615     }
616     if (richTextComposer()->textMode() == RichTextComposer::Rich) {
617         QTextCursor cursor = richTextComposer()->textCursor();
618         cursor.beginEditBlock();
619 
620         QTextCharFormat format = cursor.charFormat();
621         // Save original format to create an extra space with the existing char
622         // format for the block
623         const QTextCharFormat originalFormat = format;
624         // Add link details
625         format.setAnchor(true);
626         format.setAnchorHref(url);
627         // Workaround for QTBUG-1814:
628         // Link formatting does not get applied immediately when setAnchor(true)
629         // is called.  So the formatting needs to be applied manually.
630         format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
631         format.setUnderlineColor(d->linkColor());
632         format.setForeground(d->linkColor());
633         // Insert link text specified in dialog, otherwise write out url.
634         cursor.insertText(url, format);
635 
636         cursor.setPosition(cursor.selectionEnd());
637         cursor.setCharFormat(originalFormat);
638         cursor.insertText(QStringLiteral(" \n"));
639         cursor.endEditBlock();
640     } else {
641         richTextComposer()->textCursor().insertText(url + QLatin1Char('\n'));
642     }
643 }
644 
setCursorPositionFromStart(unsigned int pos)645 void RichTextComposerControler::setCursorPositionFromStart(unsigned int pos)
646 {
647     if (pos > 0) {
648         QTextCursor cursor = richTextComposer()->textCursor();
649         // Fix html pos cursor
650         cursor.setPosition(qMin(pos, (unsigned int)cursor.document()->characterCount() - 1));
651         richTextComposer()->setTextCursor(cursor);
652         ensureCursorVisible();
653     }
654 }
655 
ensureCursorVisible()656 void RichTextComposerControler::ensureCursorVisible()
657 {
658     // Hack: In KMail, the layout of the composer changes again after
659     //       creating the editor (the toolbar/menubar creation is delayed), so
660     //       the size of the editor changes as well, possibly hiding the cursor
661     //       even though we called ensureCursorVisible() before the layout phase.
662     //
663     //       Delay the actual call to ensureCursorVisible() a bit to work around
664     //       the problem.
665     QTimer::singleShot(500ms, richTextComposer()->composerControler(), &RichTextComposerControler::ensureCursorVisibleDelayed);
666 }
667 
fixupTextEditString(QString & text) const668 void RichTextComposerControler::RichTextComposerControlerPrivate::fixupTextEditString(QString &text) const
669 {
670     // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here
671     text.remove(QChar::LineSeparator);
672 
673     // Get rid of embedded images, see QTextImageFormat documentation:
674     // "Inline images are represented by an object replacement character (0xFFFC in Unicode) "
675     text.remove(0xFFFC);
676 
677     // In plaintext mode, each space is non-breaking.
678     text.replace(QChar::Nbsp, QChar::fromLatin1(' '));
679 }
680 
isFormattingUsed() const681 bool RichTextComposerControler::isFormattingUsed() const
682 {
683     if (richTextComposer()->textMode() == RichTextComposer::Plain) {
684         return false;
685     }
686 
687     return KPIMTextEdit::TextUtils::containsFormatting(richTextComposer()->document());
688 }
689 
slotAddEmoticon(const QString & text)690 void RichTextComposerControler::slotAddEmoticon(const QString &text)
691 {
692     QTextCursor cursor = richTextComposer()->textCursor();
693     cursor.insertText(text);
694 }
695 
slotInsertHtml()696 void RichTextComposerControler::slotInsertHtml()
697 {
698     if (richTextComposer()->textMode() == RichTextComposer::Rich) {
699         QPointer<KPIMTextEdit::InsertHtmlDialog> dialog = new KPIMTextEdit::InsertHtmlDialog(richTextComposer());
700         const QTextDocumentFragment fragmentSelected = richTextComposer()->textCursor().selection();
701         if (!fragmentSelected.isEmpty()) {
702             dialog->setSelectedText(fragmentSelected.toHtml());
703         }
704         if (dialog->exec()) {
705             const QString str = dialog->html();
706             if (!str.isEmpty()) {
707                 QTextCursor cursor = richTextComposer()->textCursor();
708                 cursor.insertHtml(str);
709             }
710         }
711         delete dialog;
712     }
713 }
714 
slotAddImage()715 void RichTextComposerControler::slotAddImage()
716 {
717     QPointer<KPIMTextEdit::InsertImageDialog> dlg = new KPIMTextEdit::InsertImageDialog(richTextComposer());
718     if (dlg->exec() == QDialog::Accepted) {
719         const QUrl url = dlg->imageUrl();
720         int imageWidth = -1;
721         int imageHeight = -1;
722         if (!dlg->keepOriginalSize()) {
723             imageWidth = dlg->imageWidth();
724             imageHeight = dlg->imageHeight();
725         }
726         if (url.isLocalFile()) {
727             d->richTextImages->addImage(url, imageWidth, imageHeight);
728         } else {
729             KMessageBox::error(richTextComposer(), i18n("Only local files are supported."));
730         }
731     }
732     delete dlg;
733 }
734 
slotFormatReset()735 void RichTextComposerControler::slotFormatReset()
736 {
737     setTextBackgroundColor(richTextComposer()->palette().highlightedText().color());
738     setTextForegroundColor(richTextComposer()->palette().text().color());
739     richTextComposer()->setFont(d->saveFont);
740 }
741 
slotDeleteLine()742 void RichTextComposerControler::slotDeleteLine()
743 {
744     if (richTextComposer()->hasFocus()) {
745         QTextCursor cursor = richTextComposer()->textCursor();
746         QTextBlock block = cursor.block();
747         const QTextLayout *layout = block.layout();
748 
749         // The current text block can have several lines due to word wrapping.
750         // Search the line the cursor is in, and then delete it.
751         for (int lineNumber = 0; lineNumber < layout->lineCount(); ++lineNumber) {
752             QTextLine line = layout->lineAt(lineNumber);
753             const bool lastLineInBlock = (line.textStart() + line.textLength() == block.length() - 1);
754             const bool oneLineBlock = (layout->lineCount() == 1);
755             const int startOfLine = block.position() + line.textStart();
756             int endOfLine = block.position() + line.textStart() + line.textLength();
757             if (!lastLineInBlock) {
758                 endOfLine -= 1;
759             }
760 
761             // Found the line where the cursor is in
762             if (cursor.position() >= startOfLine && cursor.position() <= endOfLine) {
763                 int deleteStart = startOfLine;
764                 int deleteLength = line.textLength();
765                 if (oneLineBlock) {
766                     deleteLength++; // The trailing newline
767                 }
768 
769                 // When deleting the last line in the document,
770                 // remove the newline of the line before the last line instead
771                 if (deleteStart + deleteLength >= richTextComposer()->document()->characterCount() && deleteStart > 0) {
772                     deleteStart--;
773                 }
774 
775                 cursor.beginEditBlock();
776                 cursor.setPosition(deleteStart);
777                 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength);
778                 cursor.removeSelectedText();
779                 cursor.endEditBlock();
780                 return;
781             }
782         }
783     }
784 }
785 
slotPasteAsQuotation()786 void RichTextComposerControler::slotPasteAsQuotation()
787 {
788 #ifndef QT_NO_CLIPBOARD
789     if (richTextComposer()->hasFocus()) {
790         const QString s = QApplication::clipboard()->text();
791         if (!s.isEmpty()) {
792             richTextComposer()->insertPlainText(d->addQuotesToText(s, d->richtextComposer->defaultQuoteSign()));
793         }
794     }
795 #endif
796 }
797 
slotPasteWithoutFormatting()798 void RichTextComposerControler::slotPasteWithoutFormatting()
799 {
800 #ifndef QT_NO_CLIPBOARD
801     if (richTextComposer()->hasFocus()) {
802         const QString s = QApplication::clipboard()->text();
803         if (!s.isEmpty()) {
804             richTextComposer()->insertPlainText(s);
805         }
806     }
807 #endif
808 }
809 
slotRemoveQuotes()810 void RichTextComposerControler::slotRemoveQuotes()
811 {
812     QTextCursor cursor = richTextComposer()->textCursor();
813     cursor.beginEditBlock();
814     if (!cursor.hasSelection()) {
815         cursor.select(QTextCursor::Document);
816     }
817 
818     QTextBlock block = richTextComposer()->document()->findBlock(cursor.selectionStart());
819     int selectionEnd = cursor.selectionEnd();
820     while (block.isValid() && block.position() <= selectionEnd) {
821         cursor.setPosition(block.position());
822         const int length = richTextComposer()->quoteLength(block.text(), true);
823         if (length > 0) {
824             cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, length);
825             cursor.removeSelectedText();
826             selectionEnd -= length;
827         }
828         block = block.next();
829     }
830     cursor.clearSelection();
831     cursor.endEditBlock();
832 }
833 
slotAddQuotes()834 void RichTextComposerControler::slotAddQuotes()
835 {
836     addQuotes(d->richtextComposer->defaultQuoteSign());
837 }
838 
addQuotes(const QString & defaultQuote)839 void RichTextComposerControler::addQuotes(const QString &defaultQuote)
840 {
841     QTextCursor cursor = richTextComposer()->textCursor();
842     cursor.beginEditBlock();
843     QString selectedText;
844     bool lastCharacterIsAParagraphChar = false;
845     if (!cursor.hasSelection()) {
846         cursor.select(QTextCursor::Document);
847         selectedText = cursor.selectedText();
848         cursor.removeSelectedText();
849     } else {
850         selectedText = cursor.selectedText();
851         if (selectedText[selectedText.length() - 1] == QChar::ParagraphSeparator) {
852             lastCharacterIsAParagraphChar = true;
853         }
854     }
855     QString text = d->addQuotesToText(selectedText, defaultQuote);
856     if (lastCharacterIsAParagraphChar) {
857         text += QChar::ParagraphSeparator;
858     }
859     richTextComposer()->insertPlainText(text);
860 
861     cursor.endEditBlock();
862 }
863 
addQuotesToText(const QString & inputText,const QString & defaultQuoteSign)864 QString RichTextComposerControler::RichTextComposerControlerPrivate::addQuotesToText(const QString &inputText, const QString &defaultQuoteSign)
865 {
866     QString answer = inputText;
867     answer.replace(QLatin1Char('\n'), QLatin1Char('\n') + defaultQuoteSign);
868     // cursor.selectText() as QChar::ParagraphSeparator as paragraph separator.
869     answer.replace(QChar::ParagraphSeparator, QLatin1Char('\n') + defaultQuoteSign);
870     answer.prepend(defaultQuoteSign);
871     answer += QLatin1Char('\n');
872     return richtextComposer->smartQuote(answer);
873 }
874 
slotFormatPainter(bool active)875 void RichTextComposerControler::slotFormatPainter(bool active)
876 {
877     if (active) {
878         d->painterFormat = richTextComposer()->currentCharFormat();
879         d->painterActive = true;
880         richTextComposer()->viewport()->setCursor(QCursor(QIcon::fromTheme(QStringLiteral("draw-brush")).pixmap(32, 32), 0, 32));
881     } else {
882         d->painterFormat = QTextCharFormat();
883         d->painterActive = false;
884         richTextComposer()->viewport()->setCursor(Qt::IBeamCursor);
885     }
886 }
887 
textModeChanged(KPIMTextEdit::RichTextComposer::Mode mode)888 void RichTextComposerControler::textModeChanged(KPIMTextEdit::RichTextComposer::Mode mode)
889 {
890     if (mode == KPIMTextEdit::RichTextComposer::Rich) {
891         d->saveFont = richTextComposer()->currentFont();
892     }
893 }
894 
toCleanPlainText(const QString & plainText) const895 QString RichTextComposerControler::toCleanPlainText(const QString &plainText) const
896 {
897     QString temp = plainText.isEmpty() ? richTextComposer()->toPlainText() : plainText;
898     d->fixupTextEditString(temp);
899     return temp;
900 }
901 
toWrappedPlainText() const902 QString RichTextComposerControler::toWrappedPlainText() const
903 {
904     QTextDocument *doc = richTextComposer()->document();
905     return toWrappedPlainText(doc);
906 }
907 
toWrappedPlainText(QTextDocument * doc) const908 QString RichTextComposerControler::toWrappedPlainText(QTextDocument *doc) const
909 {
910     QString temp;
911     static const QRegularExpression rx(QStringLiteral("(http|ftp|ldap)s?\\S+-$"));
912     QTextBlock block = doc->begin();
913     while (block.isValid()) {
914         QTextLayout *layout = block.layout();
915         const int numberOfLine(layout->lineCount());
916         bool urlStart = false;
917         for (int i = 0; i < numberOfLine; ++i) {
918             const QTextLine line = layout->lineAt(i);
919             const QString lineText = block.text().mid(line.textStart(), line.textLength());
920 
921             if (lineText.contains(rx) || (urlStart && !lineText.contains(QLatin1Char(' ')) && lineText.endsWith(QLatin1Char('-')))) {
922                 // don't insert line break in URL
923                 temp += lineText;
924                 urlStart = true;
925             } else {
926                 temp += lineText + QLatin1Char('\n');
927             }
928         }
929         block = block.next();
930     }
931 
932     // Remove the last superfluous newline added above
933     if (temp.endsWith(QLatin1Char('\n'))) {
934         temp.chop(1);
935     }
936     d->fixupTextEditString(temp);
937     return temp;
938 }
939