KPimTextEdit

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

KDE's Doxygen guidelines are available online.