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; \"> </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> </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