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