File indexing completed on 2024-05-19 05:21:44

0001 /*
0002    SPDX-FileCopyrightText: 2015-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "richtextcomposercontroler.h"
0008 #include "inserthtmldialog.h"
0009 #include "klinkdialog_p.h"
0010 #include "nestedlisthelper_p.h"
0011 #include "richtextcomposerimages.h"
0012 #include <QApplication>
0013 #include <QRegularExpression>
0014 
0015 #include "insertimagedialog.h"
0016 #include "textutils.h"
0017 #include <KColorScheme>
0018 #include <KLocalizedString>
0019 #include <KMessageBox>
0020 #include <QClipboard>
0021 #include <QColorDialog>
0022 #include <QIcon>
0023 #include <QPointer>
0024 #include <QRegularExpression>
0025 #include <QTextBlock>
0026 #include <QTextDocumentFragment>
0027 #include <QTextList>
0028 #include <QTimer>
0029 #include <chrono>
0030 
0031 using namespace std::chrono_literals;
0032 
0033 using namespace KPIMTextEdit;
0034 
0035 class Q_DECL_HIDDEN RichTextComposerControler::RichTextComposerControllerPrivate
0036 {
0037 public:
0038     RichTextComposerControllerPrivate(RichTextComposer *composer, RichTextComposerControler *qq)
0039         : richtextComposer(composer)
0040         , q(qq)
0041     {
0042         nestedListHelper = new NestedListHelper(composer);
0043         richTextImages = new RichTextComposerImages(richtextComposer, q);
0044     }
0045 
0046     ~RichTextComposerControllerPrivate()
0047     {
0048         delete nestedListHelper;
0049     }
0050 
0051     void regenerateColorScheme()
0052     {
0053         mLinkColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color();
0054         // TODO update existing link
0055     }
0056 
0057     QColor linkColor()
0058     {
0059         if (mLinkColor.isValid()) {
0060             return mLinkColor;
0061         }
0062         regenerateColorScheme();
0063         return mLinkColor;
0064     }
0065 
0066     void selectLinkText(QTextCursor *cursor) const;
0067     void fixupTextEditString(QString &text) const;
0068     void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
0069     [[nodiscard]] QString addQuotesToText(const QString &inputText, const QString &defaultQuoteSign);
0070     void updateLink(const QString &linkUrl, const QString &linkText);
0071     QFont saveFont;
0072     QColor mLinkColor;
0073     QTextCharFormat painterFormat;
0074     NestedListHelper *nestedListHelper = nullptr;
0075     RichTextComposer *richtextComposer = nullptr;
0076     RichTextComposerImages *richTextImages = nullptr;
0077     RichTextComposerControler *const q;
0078     bool painterActive = false;
0079 };
0080 
0081 void RichTextComposerControler::RichTextComposerControllerPrivate::selectLinkText(QTextCursor *cursor) const
0082 {
0083     // If the cursor is on a link, select the text of the link.
0084     if (cursor->charFormat().isAnchor()) {
0085         const QString aHref = cursor->charFormat().anchorHref();
0086 
0087         // Move cursor to start of link
0088         while (cursor->charFormat().anchorHref() == aHref) {
0089             if (cursor->atStart()) {
0090                 break;
0091             }
0092             cursor->setPosition(cursor->position() - 1);
0093         }
0094         if (cursor->charFormat().anchorHref() != aHref) {
0095             cursor->setPosition(cursor->position() + 1, QTextCursor::KeepAnchor);
0096         }
0097 
0098         // Move selection to the end of the link
0099         while (cursor->charFormat().anchorHref() == aHref) {
0100             if (cursor->atEnd()) {
0101                 break;
0102             }
0103             const int oldPosition = cursor->position();
0104             cursor->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
0105             // Wordaround Qt Bug. when we have a table.
0106             // FIXME selection url
0107             if (oldPosition == cursor->position()) {
0108                 break;
0109             }
0110         }
0111         if (cursor->charFormat().anchorHref() != aHref) {
0112             cursor->setPosition(cursor->position() - 1, QTextCursor::KeepAnchor);
0113         }
0114     } else if (cursor->hasSelection()) {
0115         // Nothing to do. Using the currently selected text as the link text.
0116     } else {
0117         // Select current word
0118         cursor->movePosition(QTextCursor::StartOfWord);
0119         cursor->movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
0120     }
0121 }
0122 
0123 void RichTextComposerControler::RichTextComposerControllerPrivate::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
0124 {
0125     QTextCursor cursor = richtextComposer->textCursor();
0126     QTextCursor wordStart(cursor);
0127     QTextCursor wordEnd(cursor);
0128 
0129     wordStart.movePosition(QTextCursor::StartOfWord);
0130     wordEnd.movePosition(QTextCursor::EndOfWord);
0131 
0132     cursor.beginEditBlock();
0133     if (!cursor.hasSelection() && cursor.position() != wordStart.position() && cursor.position() != wordEnd.position()) {
0134         cursor.select(QTextCursor::WordUnderCursor);
0135     }
0136     cursor.mergeCharFormat(format);
0137     richtextComposer->mergeCurrentCharFormat(format);
0138     cursor.endEditBlock();
0139 }
0140 
0141 RichTextComposerControler::RichTextComposerControler(RichTextComposer *richtextComposer, QObject *parent)
0142     : QObject(parent)
0143     , d(new RichTextComposerControllerPrivate(richtextComposer, this))
0144 {
0145 }
0146 
0147 RichTextComposerControler::~RichTextComposerControler() = default;
0148 
0149 void RichTextComposerControler::regenerateColorScheme()
0150 {
0151     d->regenerateColorScheme();
0152 }
0153 
0154 bool RichTextComposerControler::painterActive() const
0155 {
0156     return d->painterActive;
0157 }
0158 
0159 void RichTextComposerControler::addCheckbox(bool add)
0160 {
0161     QTextBlockFormat fmt;
0162     fmt.setMarker(add ? QTextBlockFormat::MarkerType::Unchecked : QTextBlockFormat::MarkerType::NoMarker);
0163     QTextCursor cursor = richTextComposer()->textCursor();
0164     cursor.beginEditBlock();
0165     if (add && !cursor.currentList()) {
0166         // Checkbox only works with lists, so if we are not at list, add a new one
0167         setListStyle(1);
0168     } else if (!add && cursor.currentList() && cursor.currentList()->count() == 1) {
0169         // If this is a single-element list with a checkbox, and user disables
0170         // a checkbox, assume user don't want a list too
0171         // (so when cursor is not on a list, and enables checkbox and disables
0172         // it right after, he returns to the same state with no list)
0173         setListStyle(0);
0174     }
0175     cursor.mergeBlockFormat(fmt);
0176     cursor.endEditBlock();
0177 }
0178 
0179 void RichTextComposerControler::setFontForWholeText(const QFont &font)
0180 {
0181     QTextCharFormat fmt;
0182     fmt.setFont(font);
0183     QTextCursor cursor(richTextComposer()->document());
0184     cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
0185     cursor.mergeCharFormat(fmt);
0186     richTextComposer()->document()->setDefaultFont(font);
0187 }
0188 
0189 void RichTextComposerControler::disablePainter()
0190 {
0191     // If the painter is active, paint the selection with the
0192     // correct format.
0193     if (richTextComposer()->textCursor().hasSelection()) {
0194         QTextCursor cursor = richTextComposer()->textCursor();
0195         cursor.setCharFormat(d->painterFormat);
0196         richTextComposer()->setTextCursor(cursor);
0197     }
0198     d->painterActive = false;
0199 }
0200 
0201 RichTextComposerImages *RichTextComposerControler::composerImages() const
0202 {
0203     return d->richTextImages;
0204 }
0205 
0206 NestedListHelper *RichTextComposerControler::nestedListHelper() const
0207 {
0208     return d->nestedListHelper;
0209 }
0210 
0211 void RichTextComposerControler::ensureCursorVisibleDelayed()
0212 {
0213     d->richtextComposer->ensureCursorVisible();
0214 }
0215 
0216 RichTextComposer *RichTextComposerControler::richTextComposer() const
0217 {
0218     return d->richtextComposer;
0219 }
0220 
0221 void RichTextComposerControler::insertHorizontalRule()
0222 {
0223     QTextCursor cursor = richTextComposer()->textCursor();
0224     QTextBlockFormat bf = cursor.blockFormat();
0225     QTextCharFormat cf = cursor.charFormat();
0226 
0227     cursor.beginEditBlock();
0228     cursor.insertHtml(QStringLiteral("<hr>"));
0229     cursor.insertBlock(bf, cf);
0230     cursor.endEditBlock();
0231     richTextComposer()->setTextCursor(cursor);
0232     richTextComposer()->activateRichText();
0233 }
0234 
0235 void RichTextComposerControler::alignLeft()
0236 {
0237     richTextComposer()->setAlignment(Qt::AlignLeft);
0238     richTextComposer()->setFocus();
0239     richTextComposer()->activateRichText();
0240 }
0241 
0242 void RichTextComposerControler::alignCenter()
0243 {
0244     richTextComposer()->setAlignment(Qt::AlignHCenter);
0245     richTextComposer()->setFocus();
0246     richTextComposer()->activateRichText();
0247 }
0248 
0249 void RichTextComposerControler::alignRight()
0250 {
0251     richTextComposer()->setAlignment(Qt::AlignRight);
0252     richTextComposer()->setFocus();
0253     richTextComposer()->activateRichText();
0254 }
0255 
0256 void RichTextComposerControler::alignJustify()
0257 {
0258     richTextComposer()->setAlignment(Qt::AlignJustify);
0259     richTextComposer()->setFocus();
0260     richTextComposer()->activateRichText();
0261 }
0262 
0263 void RichTextComposerControler::makeRightToLeft()
0264 {
0265     QTextBlockFormat format;
0266     format.setLayoutDirection(Qt::RightToLeft);
0267     QTextCursor cursor = richTextComposer()->textCursor();
0268     cursor.mergeBlockFormat(format);
0269     richTextComposer()->setTextCursor(cursor);
0270     richTextComposer()->setFocus();
0271     richTextComposer()->activateRichText();
0272 }
0273 
0274 void RichTextComposerControler::makeLeftToRight()
0275 {
0276     QTextBlockFormat format;
0277     format.setLayoutDirection(Qt::LeftToRight);
0278     QTextCursor cursor = richTextComposer()->textCursor();
0279     cursor.mergeBlockFormat(format);
0280     richTextComposer()->setTextCursor(cursor);
0281     richTextComposer()->setFocus();
0282     richTextComposer()->activateRichText();
0283 }
0284 
0285 void RichTextComposerControler::setTextBold(bool bold)
0286 {
0287     QTextCharFormat fmt;
0288     fmt.setFontWeight(bold ? QFont::Bold : QFont::Normal);
0289     d->mergeFormatOnWordOrSelection(fmt);
0290     richTextComposer()->setFocus();
0291     richTextComposer()->activateRichText();
0292 }
0293 
0294 void RichTextComposerControler::setTextItalic(bool italic)
0295 {
0296     QTextCharFormat fmt;
0297     fmt.setFontItalic(italic);
0298     d->mergeFormatOnWordOrSelection(fmt);
0299     richTextComposer()->setFocus();
0300     richTextComposer()->activateRichText();
0301 }
0302 
0303 void RichTextComposerControler::setTextUnderline(bool underline)
0304 {
0305     QTextCharFormat fmt;
0306     fmt.setFontUnderline(underline);
0307     d->mergeFormatOnWordOrSelection(fmt);
0308     richTextComposer()->setFocus();
0309     richTextComposer()->activateRichText();
0310 }
0311 
0312 void RichTextComposerControler::setTextStrikeOut(bool strikeOut)
0313 {
0314     QTextCharFormat fmt;
0315     fmt.setFontStrikeOut(strikeOut);
0316     d->mergeFormatOnWordOrSelection(fmt);
0317     richTextComposer()->setFocus();
0318     richTextComposer()->activateRichText();
0319 }
0320 
0321 void RichTextComposerControler::setTextForegroundColor(const QColor &color)
0322 {
0323     QTextCharFormat fmt;
0324     fmt.setForeground(color);
0325     d->mergeFormatOnWordOrSelection(fmt);
0326     richTextComposer()->setFocus();
0327     richTextComposer()->activateRichText();
0328 }
0329 
0330 void RichTextComposerControler::setTextBackgroundColor(const QColor &color)
0331 {
0332     QTextCharFormat fmt;
0333     fmt.setBackground(color);
0334     d->mergeFormatOnWordOrSelection(fmt);
0335     richTextComposer()->setFocus();
0336     richTextComposer()->activateRichText();
0337 }
0338 
0339 void RichTextComposerControler::setFontFamily(const QString &fontFamily)
0340 {
0341     QTextCharFormat fmt;
0342     fmt.setFontFamilies(QStringList() << fontFamily);
0343     d->mergeFormatOnWordOrSelection(fmt);
0344     richTextComposer()->setFocus();
0345     richTextComposer()->activateRichText();
0346 }
0347 
0348 void RichTextComposerControler::setFontSize(int size)
0349 {
0350     QTextCharFormat fmt;
0351     fmt.setFontPointSize(size);
0352     d->mergeFormatOnWordOrSelection(fmt);
0353     richTextComposer()->setFocus();
0354     richTextComposer()->activateRichText();
0355 }
0356 
0357 void RichTextComposerControler::setFont(const QFont &font)
0358 {
0359     QTextCharFormat fmt;
0360     fmt.setFont(font);
0361     d->mergeFormatOnWordOrSelection(fmt);
0362     richTextComposer()->setFocus();
0363     richTextComposer()->activateRichText();
0364 }
0365 
0366 void RichTextComposerControler::setTextSuperScript(bool superscript)
0367 {
0368     QTextCharFormat fmt;
0369     fmt.setVerticalAlignment(superscript ? QTextCharFormat::AlignSuperScript : QTextCharFormat::AlignNormal);
0370     d->mergeFormatOnWordOrSelection(fmt);
0371     richTextComposer()->setFocus();
0372     richTextComposer()->activateRichText();
0373 }
0374 
0375 void RichTextComposerControler::setTextSubScript(bool subscript)
0376 {
0377     QTextCharFormat fmt;
0378     fmt.setVerticalAlignment(subscript ? QTextCharFormat::AlignSubScript : QTextCharFormat::AlignNormal);
0379     d->mergeFormatOnWordOrSelection(fmt);
0380     richTextComposer()->setFocus();
0381     richTextComposer()->activateRichText();
0382 }
0383 
0384 void RichTextComposerControler::setHeadingLevel(int level)
0385 {
0386     const int boundedLevel = qBound(0, 6, level);
0387     // Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
0388     // level=2 look the same
0389     const int sizeAdjustment = boundedLevel > 0 ? 5 - boundedLevel : 0;
0390 
0391     QTextCursor cursor = richTextComposer()->textCursor();
0392     cursor.beginEditBlock();
0393 
0394     QTextBlockFormat blkfmt;
0395     blkfmt.setHeadingLevel(boundedLevel);
0396     cursor.mergeBlockFormat(blkfmt);
0397 
0398     QTextCharFormat chrfmt;
0399     chrfmt.setFontWeight(boundedLevel > 0 ? QFont::Bold : QFont::Normal);
0400     chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
0401     // Applying style to the current line or selection
0402     QTextCursor selectCursor = cursor;
0403     if (selectCursor.hasSelection()) {
0404         QTextCursor top = selectCursor;
0405         top.setPosition(qMin(top.anchor(), top.position()));
0406         top.movePosition(QTextCursor::StartOfBlock);
0407 
0408         QTextCursor bottom = selectCursor;
0409         bottom.setPosition(qMax(bottom.anchor(), bottom.position()));
0410         bottom.movePosition(QTextCursor::EndOfBlock);
0411 
0412         selectCursor.setPosition(top.position(), QTextCursor::MoveAnchor);
0413         selectCursor.setPosition(bottom.position(), QTextCursor::KeepAnchor);
0414     } else {
0415         selectCursor.select(QTextCursor::BlockUnderCursor);
0416     }
0417     selectCursor.mergeCharFormat(chrfmt);
0418 
0419     cursor.mergeBlockCharFormat(chrfmt);
0420     cursor.endEditBlock();
0421     richTextComposer()->setTextCursor(cursor);
0422     richTextComposer()->setFocus();
0423     richTextComposer()->activateRichText();
0424 }
0425 
0426 void RichTextComposerControler::setChangeTextForegroundColor()
0427 {
0428     const QColor currentColor = richTextComposer()->textColor();
0429     const QColor defaultColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground().color();
0430 
0431     const QColor selectedColor = QColorDialog::getColor(currentColor.isValid() ? currentColor : defaultColor, richTextComposer());
0432 
0433     if (!selectedColor.isValid() && !currentColor.isValid()) {
0434         setTextForegroundColor(defaultColor);
0435     } else if (selectedColor.isValid()) {
0436         setTextForegroundColor(selectedColor);
0437     }
0438 }
0439 
0440 void RichTextComposerControler::setChangeTextBackgroundColor()
0441 {
0442     QTextCharFormat fmt = richTextComposer()->textCursor().charFormat();
0443     const QColor currentColor = fmt.background().color();
0444     const QColor defaultColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground().color();
0445 
0446     const QColor selectedColor = QColorDialog::getColor(currentColor.isValid() ? currentColor : defaultColor, richTextComposer());
0447 
0448     if (!selectedColor.isValid() && !currentColor.isValid()) {
0449         setTextBackgroundColor(defaultColor);
0450     } else if (selectedColor.isValid()) {
0451         setTextBackgroundColor(selectedColor);
0452     }
0453 }
0454 
0455 QString RichTextComposerControler::currentLinkUrl() const
0456 {
0457     return richTextComposer()->textCursor().charFormat().anchorHref();
0458 }
0459 
0460 QString RichTextComposerControler::currentLinkText() const
0461 {
0462     QTextCursor cursor = richTextComposer()->textCursor();
0463     d->selectLinkText(&cursor);
0464     return cursor.selectedText();
0465 }
0466 
0467 void RichTextComposerControler::selectLinkText() const
0468 {
0469     QTextCursor cursor = richTextComposer()->textCursor();
0470     d->selectLinkText(&cursor);
0471     richTextComposer()->setTextCursor(cursor);
0472 }
0473 
0474 void RichTextComposerControler::manageLink()
0475 {
0476     selectLinkText();
0477     QPointer<KLinkDialog> linkDialog = new KLinkDialog(richTextComposer());
0478     linkDialog->setLinkText(currentLinkText());
0479     linkDialog->setLinkUrl(currentLinkUrl());
0480 
0481     if (linkDialog->exec()) {
0482         d->updateLink(linkDialog->linkUrl(), linkDialog->linkText());
0483     }
0484 
0485     delete linkDialog;
0486 }
0487 
0488 void RichTextComposerControler::updateLink(const QString &linkUrl, const QString &linkText)
0489 {
0490     d->updateLink(linkUrl, linkText);
0491 }
0492 
0493 void RichTextComposerControler::RichTextComposerControllerPrivate::updateLink(const QString &linkUrl, const QString &linkText)
0494 {
0495     q->selectLinkText();
0496 
0497     QTextCursor cursor = richtextComposer->textCursor();
0498     cursor.beginEditBlock();
0499 
0500     if (!cursor.hasSelection()) {
0501         cursor.select(QTextCursor::WordUnderCursor);
0502     }
0503 
0504     QTextCharFormat format = cursor.charFormat();
0505     // Save original format to create an extra space with the existing char
0506     // format for the block
0507     if (!linkUrl.isEmpty()) {
0508         // Add link details
0509         format.setAnchor(true);
0510         format.setAnchorHref(linkUrl);
0511         // Workaround for QTBUG-1814:
0512         // Link formatting does not get applied immediately when setAnchor(true)
0513         // is called.  So the formatting needs to be applied manually.
0514         format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
0515         format.setUnderlineColor(linkColor());
0516         format.setForeground(linkColor());
0517         richtextComposer->activateRichText();
0518     } else {
0519         // Remove link details
0520         format.setAnchor(false);
0521         format.setAnchorHref(QString());
0522         // Workaround for QTBUG-1814:
0523         // Link formatting does not get removed immediately when setAnchor(false)
0524         // is called. So the formatting needs to be applied manually.
0525         QTextDocument defaultTextDocument;
0526         QTextCharFormat defaultCharFormat = defaultTextDocument.begin().charFormat();
0527 
0528         format.setUnderlineStyle(defaultCharFormat.underlineStyle());
0529         format.setUnderlineColor(defaultCharFormat.underlineColor());
0530         format.setForeground(defaultCharFormat.foreground());
0531     }
0532 
0533     // Insert link text specified in dialog, otherwise write out url.
0534     QString _linkText;
0535     if (!linkText.isEmpty()) {
0536         _linkText = linkText;
0537     } else {
0538         _linkText = linkUrl;
0539     }
0540     cursor.insertText(_linkText, format);
0541 
0542     cursor.endEditBlock();
0543 }
0544 
0545 QString RichTextComposerControler::toCleanHtml() const
0546 {
0547     QString result = richTextComposer()->toHtml();
0548 
0549     static const QString EMPTYLINEHTML = QStringLiteral(
0550         "<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; "
0551         "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; \">&nbsp;</p>");
0552 
0553     // Qt inserts various style properties based on the current mode of the editor (underline,
0554     // bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'.
0555     static const QRegularExpression EMPTYLINEREGEX(QStringLiteral("<p style=\"-qt-paragraph-type:empty;(.*?)</p>"));
0556 
0557     static const QString OLLISTPATTERNQT = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
0558 
0559     static const QString ULLISTPATTERNQT = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
0560 
0561     static const QString ORDEREDLISTHTML = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px;");
0562 
0563     static const QString UNORDEREDLISTHTML = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px;");
0564 
0565     // fix 1 - empty lines should show as empty lines - MS Outlook treats margin-top:0px; as
0566     // a non-existing line.
0567     // Although we can simply remove the margin-top style property, we still get unwanted results
0568     // if you have three or more empty lines. It's best to replace empty <p> elements with <p>&nbsp;</p>.
0569 
0570     // replace all the matching text with the new line text
0571     result.replace(EMPTYLINEREGEX, EMPTYLINEHTML);
0572 
0573     // fix 2a - ordered lists - MS Outlook treats margin-left:0px; as
0574     // a non-existing number; e.g: "1. First item" turns into "First Item"
0575     result.replace(OLLISTPATTERNQT, ORDEREDLISTHTML);
0576 
0577     // fix 2b - unordered lists - MS Outlook treats margin-left:0px; as
0578     // a non-existing bullet; e.g: "* First bullet" turns into "First Bullet"
0579     result.replace(ULLISTPATTERNQT, UNORDEREDLISTHTML);
0580 
0581     return result;
0582 }
0583 
0584 bool RichTextComposerControler::canIndentList() const
0585 {
0586     return d->nestedListHelper->canIndent();
0587 }
0588 
0589 bool RichTextComposerControler::canDedentList() const
0590 {
0591     return d->nestedListHelper->canDedent();
0592 }
0593 
0594 void RichTextComposerControler::indentListMore()
0595 {
0596     d->nestedListHelper->handleOnIndentMore();
0597     richTextComposer()->activateRichText();
0598 }
0599 
0600 void RichTextComposerControler::indentListLess()
0601 {
0602     d->nestedListHelper->handleOnIndentLess();
0603 }
0604 
0605 void RichTextComposerControler::setListStyle(int styleIndex)
0606 {
0607     d->nestedListHelper->handleOnBulletType(-styleIndex);
0608     richTextComposer()->setFocus();
0609     richTextComposer()->activateRichText();
0610 }
0611 
0612 void RichTextComposerControler::insertLink(const QString &url)
0613 {
0614     if (url.isEmpty()) {
0615         return;
0616     }
0617     if (richTextComposer()->textMode() == RichTextComposer::Rich) {
0618         QTextCursor cursor = richTextComposer()->textCursor();
0619         cursor.beginEditBlock();
0620 
0621         QTextCharFormat format = cursor.charFormat();
0622         // Save original format to create an extra space with the existing char
0623         // format for the block
0624         const QTextCharFormat originalFormat = format;
0625         // Add link details
0626         format.setAnchor(true);
0627         format.setAnchorHref(url);
0628         // Workaround for QTBUG-1814:
0629         // Link formatting does not get applied immediately when setAnchor(true)
0630         // is called.  So the formatting needs to be applied manually.
0631         format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
0632         format.setUnderlineColor(d->linkColor());
0633         format.setForeground(d->linkColor());
0634         // Insert link text specified in dialog, otherwise write out url.
0635         cursor.insertText(url, format);
0636 
0637         cursor.setPosition(cursor.selectionEnd());
0638         cursor.setCharFormat(originalFormat);
0639         cursor.insertText(QStringLiteral(" \n"));
0640         cursor.endEditBlock();
0641     } else {
0642         richTextComposer()->textCursor().insertText(url + QLatin1Char('\n'));
0643     }
0644 }
0645 
0646 void RichTextComposerControler::setCursorPositionFromStart(unsigned int pos)
0647 {
0648     if (pos > 0) {
0649         QTextCursor cursor = richTextComposer()->textCursor();
0650         // Fix html pos cursor
0651         cursor.setPosition(qMin(pos, (unsigned int)cursor.document()->characterCount() - 1));
0652         richTextComposer()->setTextCursor(cursor);
0653         ensureCursorVisible();
0654     }
0655 }
0656 
0657 void RichTextComposerControler::ensureCursorVisible()
0658 {
0659     // Hack: In KMail, the layout of the composer changes again after
0660     //       creating the editor (the toolbar/menubar creation is delayed), so
0661     //       the size of the editor changes as well, possibly hiding the cursor
0662     //       even though we called ensureCursorVisible() before the layout phase.
0663     //
0664     //       Delay the actual call to ensureCursorVisible() a bit to work around
0665     //       the problem.
0666     QTimer::singleShot(500ms, richTextComposer()->composerControler(), &RichTextComposerControler::ensureCursorVisibleDelayed);
0667 }
0668 
0669 void RichTextComposerControler::RichTextComposerControllerPrivate::fixupTextEditString(QString &text) const
0670 {
0671     // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here
0672     text.remove(QChar::LineSeparator);
0673 
0674     // Get rid of embedded images, see QTextImageFormat documentation:
0675     // "Inline images are represented by an object replacement character (0xFFFC in Unicode) "
0676     text.remove(QChar(0xFFFC));
0677 
0678     // In plaintext mode, each space is non-breaking.
0679     text.replace(QChar::Nbsp, QChar::fromLatin1(' '));
0680 }
0681 
0682 bool RichTextComposerControler::isFormattingUsed() const
0683 {
0684     if (richTextComposer()->textMode() == RichTextComposer::Plain) {
0685         return false;
0686     }
0687 
0688     return KPIMTextEdit::TextUtils::containsFormatting(richTextComposer()->document());
0689 }
0690 
0691 void RichTextComposerControler::slotAddEmoticon(const QString &text)
0692 {
0693     QTextCursor cursor = richTextComposer()->textCursor();
0694     cursor.insertText(text);
0695 }
0696 
0697 void RichTextComposerControler::slotInsertHtml()
0698 {
0699     if (richTextComposer()->textMode() == RichTextComposer::Rich) {
0700         QPointer<KPIMTextEdit::InsertHtmlDialog> dialog = new KPIMTextEdit::InsertHtmlDialog(richTextComposer());
0701         const QTextDocumentFragment fragmentSelected = richTextComposer()->textCursor().selection();
0702         if (!fragmentSelected.isEmpty()) {
0703             dialog->setSelectedText(fragmentSelected.toHtml());
0704         }
0705         if (dialog->exec()) {
0706             const QString str = dialog->html();
0707             if (!str.isEmpty()) {
0708                 QTextCursor cursor = richTextComposer()->textCursor();
0709                 cursor.insertHtml(str);
0710             }
0711         }
0712         delete dialog;
0713     }
0714 }
0715 
0716 void RichTextComposerControler::slotAddImage()
0717 {
0718     QPointer<KPIMTextEdit::InsertImageDialog> dlg = new KPIMTextEdit::InsertImageDialog(richTextComposer());
0719     if (dlg->exec() == QDialog::Accepted) {
0720         const QUrl url = dlg->imageUrl();
0721         int imageWidth = -1;
0722         int imageHeight = -1;
0723         if (!dlg->keepOriginalSize()) {
0724             imageWidth = dlg->imageWidth();
0725             imageHeight = dlg->imageHeight();
0726         }
0727         if (url.isLocalFile()) {
0728             d->richTextImages->addImage(url, imageWidth, imageHeight);
0729         } else {
0730             KMessageBox::error(richTextComposer(), i18n("Only local files are supported."));
0731         }
0732     }
0733     delete dlg;
0734 }
0735 
0736 void RichTextComposerControler::slotFormatReset()
0737 {
0738     setTextBackgroundColor(richTextComposer()->palette().highlightedText().color());
0739     setTextForegroundColor(richTextComposer()->palette().text().color());
0740     richTextComposer()->setFont(d->saveFont);
0741 }
0742 
0743 void RichTextComposerControler::slotPasteAsQuotation()
0744 {
0745 #ifndef QT_NO_CLIPBOARD
0746     if (richTextComposer()->hasFocus()) {
0747         const QString s = QApplication::clipboard()->text();
0748         if (!s.isEmpty()) {
0749             richTextComposer()->insertPlainText(d->addQuotesToText(s, d->richtextComposer->defaultQuoteSign()));
0750         }
0751     }
0752 #endif
0753 }
0754 
0755 void RichTextComposerControler::slotPasteWithoutFormatting()
0756 {
0757 #ifndef QT_NO_CLIPBOARD
0758     if (richTextComposer()->hasFocus()) {
0759         const QString s = QApplication::clipboard()->text();
0760         if (!s.isEmpty()) {
0761             richTextComposer()->insertPlainText(s);
0762         }
0763     }
0764 #endif
0765 }
0766 
0767 void RichTextComposerControler::slotRemoveQuotes()
0768 {
0769     QTextCursor cursor = richTextComposer()->textCursor();
0770     cursor.beginEditBlock();
0771     if (!cursor.hasSelection()) {
0772         cursor.select(QTextCursor::Document);
0773     }
0774 
0775     QTextBlock block = richTextComposer()->document()->findBlock(cursor.selectionStart());
0776     int selectionEnd = cursor.selectionEnd();
0777     while (block.isValid() && block.position() <= selectionEnd) {
0778         cursor.setPosition(block.position());
0779         const int length = richTextComposer()->quoteLength(block.text(), true);
0780         if (length > 0) {
0781             cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, length);
0782             cursor.removeSelectedText();
0783             selectionEnd -= length;
0784         }
0785         block = block.next();
0786     }
0787     cursor.clearSelection();
0788     cursor.endEditBlock();
0789 }
0790 
0791 void RichTextComposerControler::slotAddQuotes()
0792 {
0793     addQuotes(d->richtextComposer->defaultQuoteSign());
0794 }
0795 
0796 void RichTextComposerControler::addQuotes(const QString &defaultQuote)
0797 {
0798     QTextCursor cursor = richTextComposer()->textCursor();
0799     cursor.beginEditBlock();
0800     QString selectedText;
0801     bool lastCharacterIsAParagraphChar = false;
0802     if (!cursor.hasSelection()) {
0803         cursor.select(QTextCursor::Document);
0804         selectedText = cursor.selectedText();
0805         cursor.removeSelectedText();
0806     } else {
0807         selectedText = cursor.selectedText();
0808         if (selectedText[selectedText.length() - 1] == QChar::ParagraphSeparator) {
0809             lastCharacterIsAParagraphChar = true;
0810         }
0811     }
0812     QString text = d->addQuotesToText(selectedText, defaultQuote);
0813     if (lastCharacterIsAParagraphChar) {
0814         text += QChar::ParagraphSeparator;
0815     }
0816     richTextComposer()->insertPlainText(text);
0817 
0818     cursor.endEditBlock();
0819 }
0820 
0821 QString RichTextComposerControler::RichTextComposerControllerPrivate::addQuotesToText(const QString &inputText, const QString &defaultQuoteSign)
0822 {
0823     QString answer = inputText;
0824     answer.replace(QLatin1Char('\n'), QLatin1Char('\n') + defaultQuoteSign);
0825     // cursor.selectText() as QChar::ParagraphSeparator as paragraph separator.
0826     answer.replace(QChar::ParagraphSeparator, QLatin1Char('\n') + defaultQuoteSign);
0827     answer.prepend(defaultQuoteSign);
0828     answer += QLatin1Char('\n');
0829     return richtextComposer->smartQuote(answer);
0830 }
0831 
0832 void RichTextComposerControler::slotFormatPainter(bool active)
0833 {
0834     if (active) {
0835         d->painterFormat = richTextComposer()->currentCharFormat();
0836         d->painterActive = true;
0837         richTextComposer()->viewport()->setCursor(QCursor(QIcon::fromTheme(QStringLiteral("draw-brush")).pixmap(32, 32), 0, 32));
0838     } else {
0839         d->painterFormat = QTextCharFormat();
0840         d->painterActive = false;
0841         richTextComposer()->viewport()->setCursor(Qt::IBeamCursor);
0842     }
0843 }
0844 
0845 void RichTextComposerControler::textModeChanged(KPIMTextEdit::RichTextComposer::Mode mode)
0846 {
0847     if (mode == KPIMTextEdit::RichTextComposer::Rich) {
0848         d->saveFont = richTextComposer()->currentFont();
0849     }
0850 }
0851 
0852 QString RichTextComposerControler::toCleanPlainText(const QString &plainText) const
0853 {
0854     QString temp = plainText.isEmpty() ? richTextComposer()->toPlainText() : plainText;
0855     d->fixupTextEditString(temp);
0856     return temp;
0857 }
0858 
0859 QString RichTextComposerControler::toWrappedPlainText() const
0860 {
0861     QTextDocument *doc = richTextComposer()->document();
0862     return toWrappedPlainText(doc);
0863 }
0864 
0865 QString RichTextComposerControler::toWrappedPlainText(QTextDocument *doc) const
0866 {
0867     QString temp;
0868     static const QRegularExpression rx(QStringLiteral("(http|ftp|ldap)s?\\S+-$"));
0869     QTextBlock block = doc->begin();
0870     while (block.isValid()) {
0871         QTextLayout *layout = block.layout();
0872         const int numberOfLine(layout->lineCount());
0873         bool urlStart = false;
0874         for (int i = 0; i < numberOfLine; ++i) {
0875             const QTextLine line = layout->lineAt(i);
0876             const QString lineText = block.text().mid(line.textStart(), line.textLength());
0877 
0878             if (lineText.contains(rx) || (urlStart && !lineText.contains(QLatin1Char(' ')) && lineText.endsWith(QLatin1Char('-')))) {
0879                 // don't insert line break in URL
0880                 temp += lineText;
0881                 urlStart = true;
0882             } else {
0883                 temp += lineText + QLatin1Char('\n');
0884             }
0885         }
0886         block = block.next();
0887     }
0888 
0889     // Remove the last superfluous newline added above
0890     if (temp.endsWith(QLatin1Char('\n'))) {
0891         temp.chop(1);
0892     }
0893     d->fixupTextEditString(temp);
0894     return temp;
0895 }
0896 
0897 bool RichTextComposerControler::event(QEvent *ev)
0898 {
0899     if (ev->type() == QEvent::ApplicationPaletteChange) {
0900         regenerateColorScheme();
0901     }
0902 
0903     return QObject::event(ev);
0904 }
0905 
0906 #include "moc_richtextcomposercontroler.cpp"