KPimTextEdit

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

KDE's Doxygen guidelines are available online.