File indexing completed on 2024-06-23 05:18:25
0001 /* 0002 SPDX-FileCopyrightText: 2015-2024 Laurent Montel <montel@kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "richtextcomposerng.h" 0008 #include "richtextcomposersignatures.h" 0009 #include "settings/messagecomposersettings.h" 0010 #include <KPIMTextEdit/MarkupDirector> 0011 #include <KPIMTextEdit/PlainTextMarkupBuilder> 0012 #include <KPIMTextEdit/RichTextComposerControler> 0013 #include <KPIMTextEdit/RichTextComposerImages> 0014 #include <KPIMTextEdit/TextHTMLBuilder> 0015 0016 #include <TextAutoCorrectionCore/AutoCorrection> 0017 0018 #include "part/textpart.h" 0019 0020 #include <KMessageBox> 0021 0022 #include <QRegularExpression> 0023 0024 #define USE_TEXTHTML_BUILDER 1 0025 0026 using namespace MessageComposer; 0027 0028 class MessageComposer::RichTextComposerNgPrivate 0029 { 0030 public: 0031 explicit RichTextComposerNgPrivate(RichTextComposerNg *q) 0032 : richtextComposer(q) 0033 , richTextComposerSignatures(new MessageComposer::RichTextComposerSignatures(richtextComposer, richtextComposer)) 0034 { 0035 } 0036 0037 void fixHtmlFontSize(QString &cleanHtml) const; 0038 [[nodiscard]] QString toCleanHtml() const; 0039 TextAutoCorrectionCore::AutoCorrection *autoCorrection = nullptr; 0040 RichTextComposerNg *const richtextComposer; 0041 MessageComposer::RichTextComposerSignatures *const richTextComposerSignatures; 0042 }; 0043 0044 RichTextComposerNg::RichTextComposerNg(QWidget *parent) 0045 : KPIMTextEdit::RichTextComposer(parent) 0046 , d(new MessageComposer::RichTextComposerNgPrivate(this)) 0047 { 0048 } 0049 0050 RichTextComposerNg::~RichTextComposerNg() = default; 0051 0052 MessageComposer::RichTextComposerSignatures *RichTextComposerNg::composerSignature() const 0053 { 0054 return d->richTextComposerSignatures; 0055 } 0056 0057 TextAutoCorrectionCore::AutoCorrection *RichTextComposerNg::autocorrection() const 0058 { 0059 return d->autoCorrection; 0060 } 0061 0062 void RichTextComposerNg::setAutocorrection(TextAutoCorrectionCore::AutoCorrection *autocorrect) 0063 { 0064 d->autoCorrection = autocorrect; 0065 } 0066 0067 void RichTextComposerNg::setAutocorrectionLanguage(const QString &lang) 0068 { 0069 if (d->autoCorrection) { 0070 TextAutoCorrectionCore::AutoCorrectionSettings *settings = d->autoCorrection->autoCorrectionSettings(); 0071 settings->setLanguage(lang); 0072 d->autoCorrection->setAutoCorrectionSettings(settings); 0073 } 0074 } 0075 0076 static bool isSpecial(const QTextCharFormat &charFormat) 0077 { 0078 return charFormat.isFrameFormat() || charFormat.isImageFormat() || charFormat.isListFormat() || charFormat.isTableFormat() 0079 || charFormat.isTableCellFormat(); 0080 } 0081 0082 bool RichTextComposerNg::processModifyText(QKeyEvent *e) 0083 { 0084 if (d->autoCorrection && d->autoCorrection->autoCorrectionSettings()->isEnabledAutoCorrection()) { 0085 if ((e->key() == Qt::Key_Space) || (e->key() == Qt::Key_Enter) || (e->key() == Qt::Key_Return)) { 0086 if (!isLineQuoted(textCursor().block().text()) && !textCursor().hasSelection()) { 0087 const QTextCharFormat initialTextFormat = textCursor().charFormat(); 0088 const bool richText = (textMode() == RichTextComposer::Rich); 0089 int position = textCursor().position(); 0090 const bool addSpace = d->autoCorrection->autocorrect(richText, *document(), position); 0091 QTextCursor cur = textCursor(); 0092 cur.setPosition(position); 0093 const bool spacePressed = (e->key() == Qt::Key_Space); 0094 if (overwriteMode() && spacePressed) { 0095 if (addSpace) { 0096 const QChar insertChar = QLatin1Char(' '); 0097 if (!cur.atBlockEnd()) { 0098 cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1); 0099 } 0100 if (richText && !isSpecial(initialTextFormat)) { 0101 cur.insertText(insertChar, initialTextFormat); 0102 } else { 0103 cur.insertText(insertChar); 0104 } 0105 setTextCursor(cur); 0106 } 0107 } else { 0108 const QChar insertChar = spacePressed ? QLatin1Char(' ') : QLatin1Char('\n'); 0109 if (richText && !isSpecial(initialTextFormat)) { 0110 if ((spacePressed && addSpace) || !spacePressed) { 0111 cur.insertText(insertChar, initialTextFormat); 0112 } 0113 } else { 0114 if ((spacePressed && addSpace) || !spacePressed) { 0115 cur.insertText(insertChar); 0116 } 0117 } 0118 setTextCursor(cur); 0119 } 0120 return true; 0121 } 0122 } 0123 } 0124 return false; 0125 } 0126 0127 void RichTextComposerNgPrivate::fixHtmlFontSize(QString &cleanHtml) const 0128 { 0129 // non-greedy matching 0130 static const QRegularExpression styleRegex(QStringLiteral("<span style=\".*?font-size:(.*?)pt;.*?</span>")); 0131 0132 QRegularExpressionMatch rmatch; 0133 int offset = 0; 0134 while (cleanHtml.indexOf(styleRegex, offset, &rmatch) != -1) { 0135 QString replacement; 0136 bool ok = false; 0137 const double ptValue = rmatch.captured(1).toDouble(&ok); 0138 if (ok) { 0139 const double emValue = ptValue / 12; 0140 replacement = QString::number(emValue, 'g', 2); 0141 const int capLen = rmatch.capturedLength(1); 0142 cleanHtml.replace(rmatch.capturedStart(1), capLen + 2 /* QLatin1StringView("pt").size() */, replacement + QLatin1StringView("em")); 0143 // advance the offset to just after the last replace 0144 offset = rmatch.capturedEnd(0) - capLen + replacement.size(); 0145 } else { 0146 // a match was found but the toDouble call failed, advance the offset to just after 0147 // the entire match 0148 offset = rmatch.capturedEnd(0); 0149 } 0150 } 0151 } 0152 0153 MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus RichTextComposerNg::convertPlainText(MessageComposer::TextPart *textPart) 0154 { 0155 Q_UNUSED(textPart) 0156 return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted; 0157 } 0158 0159 void RichTextComposerNg::fillComposerTextPart(MessageComposer::TextPart *textPart) 0160 { 0161 const bool wasConverted = convertPlainText(textPart) == MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Converted; 0162 if (composerControler()->isFormattingUsed()) { 0163 if (!wasConverted) { 0164 if (MessageComposer::MessageComposerSettings::self()->improvePlainTextOfHtmlMessage()) { 0165 auto pb = new KPIMTextEdit::PlainTextMarkupBuilder(); 0166 0167 auto pmd = new KPIMTextEdit::MarkupDirector(pb); 0168 pmd->processDocument(document()); 0169 const QString plainText = pb->getResult(); 0170 textPart->setCleanPlainText(composerControler()->toCleanPlainText(plainText)); 0171 auto doc = new QTextDocument(plainText); 0172 doc->adjustSize(); 0173 0174 textPart->setWrappedPlainText(composerControler()->toWrappedPlainText(doc)); 0175 delete doc; 0176 delete pmd; 0177 delete pb; 0178 } else { 0179 textPart->setCleanPlainText(composerControler()->toCleanPlainText()); 0180 textPart->setWrappedPlainText(composerControler()->toWrappedPlainText()); 0181 } 0182 } 0183 } else { 0184 if (!wasConverted) { 0185 textPart->setCleanPlainText(composerControler()->toCleanPlainText()); 0186 textPart->setWrappedPlainText(composerControler()->toWrappedPlainText()); 0187 } 0188 } 0189 textPart->setWordWrappingEnabled(lineWrapMode() == QTextEdit::FixedColumnWidth); 0190 if (composerControler()->isFormattingUsed() && !wasConverted) { 0191 #ifdef USE_TEXTHTML_BUILDER 0192 auto pb = new KPIMTextEdit::TextHTMLBuilder(); 0193 0194 auto pmd = new KPIMTextEdit::MarkupDirector(pb); 0195 pmd->processDocument(document()); 0196 QString cleanHtml = 0197 QStringLiteral("<html>\n<head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n</head>\n<body>%1</body>\n</html>") 0198 .arg(pb->getResult()); 0199 delete pmd; 0200 delete pb; 0201 d->fixHtmlFontSize(cleanHtml); 0202 textPart->setCleanHtml(cleanHtml); 0203 // qDebug() << " cleanHtml grantlee builder" << cleanHtml; 0204 // qDebug() << " d->toCleanHtml() " << d->toCleanHtml(); 0205 #else 0206 QString cleanHtml = d->toCleanHtml(); 0207 d->fixHtmlFontSize(cleanHtml); 0208 textPart->setCleanHtml(cleanHtml); 0209 qDebug() << "cleanHtml " << cleanHtml; 0210 #endif 0211 textPart->setEmbeddedImages(composerControler()->composerImages()->embeddedImages()); 0212 } 0213 } 0214 0215 QString RichTextComposerNgPrivate::toCleanHtml() const 0216 { 0217 QString result = richtextComposer->toHtml(); 0218 0219 static const QString EMPTYLINEHTML = QStringLiteral( 0220 "<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; " 0221 "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; \"> </p>"); 0222 0223 // Qt inserts various style properties based on the current mode of the editor (underline, 0224 // bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'. 0225 // Minimal/non-greedy matching 0226 static const QString EMPTYLINEREGEX = QStringLiteral("<p style=\"-qt-paragraph-type:empty;(?:.*?)</p>"); 0227 0228 static const QString OLLISTPATTERNQT = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;"); 0229 0230 static const QString ULLISTPATTERNQT = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;"); 0231 0232 static const QString ORDEREDLISTHTML = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px;"); 0233 0234 static const QString UNORDEREDLISTHTML = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px;"); 0235 0236 // fix 1 - empty lines should show as empty lines - MS Outlook treats margin-top:0px; as 0237 // a non-existing line. 0238 // Although we can simply remove the margin-top style property, we still get unwanted results 0239 // if you have three or more empty lines. It's best to replace empty <p> elements with <p> </p>. 0240 0241 // Replace all the matching text with the new line text 0242 result.replace(QRegularExpression(EMPTYLINEREGEX), EMPTYLINEHTML); 0243 0244 // fix 2a - ordered lists - MS Outlook treats margin-left:0px; as 0245 // a non-existing number; e.g: "1. First item" turns into "First Item" 0246 result.replace(OLLISTPATTERNQT, ORDEREDLISTHTML); 0247 0248 // fix 2b - unordered lists - MS Outlook treats margin-left:0px; as 0249 // a non-existing bullet; e.g: "* First bullet" turns into "First Bullet" 0250 result.replace(ULLISTPATTERNQT, UNORDEREDLISTHTML); 0251 0252 return result; 0253 } 0254 0255 static bool isCursorAtEndOfLine(const QTextCursor &cursor) 0256 { 0257 QTextCursor testCursor = cursor; 0258 testCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor); 0259 return !testCursor.hasSelection(); 0260 } 0261 0262 static void insertSignatureHelper(const QString &signature, 0263 RichTextComposerNg *textEdit, 0264 KIdentityManagementCore::Signature::Placement placement, 0265 bool isHtml, 0266 bool addNewlines) 0267 { 0268 if (!signature.isEmpty()) { 0269 // Save the modified state of the document, as inserting a signature 0270 // shouldn't change this. Restore it at the end of this function. 0271 bool isModified = textEdit->document()->isModified(); 0272 0273 // Move to the desired position, where the signature should be inserted 0274 QTextCursor cursor = textEdit->textCursor(); 0275 QTextCursor oldCursor = cursor; 0276 cursor.beginEditBlock(); 0277 0278 if (placement == KIdentityManagementCore::Signature::End) { 0279 cursor.movePosition(QTextCursor::End); 0280 } else if (placement == KIdentityManagementCore::Signature::Start) { 0281 cursor.movePosition(QTextCursor::Start); 0282 } else if (placement == KIdentityManagementCore::Signature::AtCursor) { 0283 cursor.movePosition(QTextCursor::StartOfLine); 0284 } 0285 textEdit->setTextCursor(cursor); 0286 0287 QString lineSep; 0288 if (addNewlines) { 0289 if (isHtml) { 0290 lineSep = QStringLiteral("<br>"); 0291 } else { 0292 lineSep = QLatin1Char('\n'); 0293 } 0294 } 0295 0296 // Insert the signature and newlines depending on where it was inserted. 0297 int newCursorPos = -1; 0298 QString headSep; 0299 QString tailSep; 0300 0301 if (placement == KIdentityManagementCore::Signature::End) { 0302 // There is one special case when re-setting the old cursor: The cursor 0303 // was at the end. In this case, QTextEdit has no way to know 0304 // if the signature was added before or after the cursor, and just 0305 // decides that it was added before (and the cursor moves to the end, 0306 // but it should not when appending a signature). See bug 167961 0307 if (oldCursor.position() == textEdit->toPlainText().length()) { 0308 newCursorPos = oldCursor.position(); 0309 } 0310 headSep = lineSep; 0311 } else if (placement == KIdentityManagementCore::Signature::Start) { 0312 // When prepending signatures, add a couple of new lines before 0313 // the signature, and move the cursor to the beginning of the QTextEdit. 0314 // People tends to insert new text there. 0315 newCursorPos = 0; 0316 headSep = lineSep + lineSep; 0317 if (!isCursorAtEndOfLine(cursor)) { 0318 tailSep = lineSep; 0319 } 0320 } else if (placement == KIdentityManagementCore::Signature::AtCursor) { 0321 if (!isCursorAtEndOfLine(cursor)) { 0322 tailSep = lineSep; 0323 } 0324 } 0325 0326 const QString full_signature = headSep + signature + tailSep; 0327 if (isHtml) { 0328 textEdit->insertHtml(full_signature); 0329 } else { 0330 textEdit->insertPlainText(full_signature); 0331 } 0332 0333 cursor.endEditBlock(); 0334 if (newCursorPos != -1) { 0335 oldCursor.setPosition(newCursorPos); 0336 } 0337 0338 textEdit->setTextCursor(oldCursor); 0339 textEdit->ensureCursorVisible(); 0340 0341 textEdit->document()->setModified(isModified); 0342 0343 if (isHtml) { 0344 textEdit->activateRichText(); 0345 } 0346 } 0347 } 0348 0349 void RichTextComposerNg::insertSignature(const KIdentityManagementCore::Signature &signature, 0350 KIdentityManagementCore::Signature::Placement placement, 0351 KIdentityManagementCore::Signature::AddedText addedText) 0352 { 0353 if (signature.isEnabledSignature()) { 0354 QString signatureStr; 0355 bool ok = false; 0356 QString errorMessage; 0357 if (addedText & KIdentityManagementCore::Signature::AddSeparator) { 0358 signatureStr = signature.withSeparator(&ok, &errorMessage); 0359 } else { 0360 signatureStr = signature.rawText(&ok, &errorMessage); 0361 } 0362 0363 if (!ok && !errorMessage.isEmpty()) { 0364 KMessageBox::error(nullptr, errorMessage); 0365 } 0366 0367 insertSignatureHelper(signatureStr, 0368 this, 0369 placement, 0370 (signature.isInlinedHtml() && signature.type() == KIdentityManagementCore::Signature::Inlined), 0371 (addedText & KIdentityManagementCore::Signature::AddNewLines)); 0372 0373 // We added the text of the signature above, now it is time to add the images as well. 0374 if (signature.isInlinedHtml()) { 0375 const QList<KIdentityManagementCore::Signature::EmbeddedImagePtr> embeddedImages = signature.embeddedImages(); 0376 for (const KIdentityManagementCore::Signature::EmbeddedImagePtr &image : embeddedImages) { 0377 composerControler()->composerImages()->loadImage(image->image, image->name, image->name); 0378 } 0379 } 0380 } 0381 } 0382 0383 QString RichTextComposerNg::toCleanHtml() const 0384 { 0385 return d->toCleanHtml(); 0386 } 0387 0388 void RichTextComposerNg::fixHtmlFontSize(QString &cleanHtml) const 0389 { 0390 d->fixHtmlFontSize(cleanHtml); 0391 } 0392 0393 void RichTextComposerNg::forceAutoCorrection(bool selectedText) 0394 { 0395 if (document()->isEmpty()) { 0396 return; 0397 } 0398 if (d->autoCorrection && d->autoCorrection->autoCorrectionSettings()->isEnabledAutoCorrection()) { 0399 const bool richText = (textMode() == RichTextComposer::Rich); 0400 const int initialPosition = textCursor().position(); 0401 QTextCursor cur = textCursor(); 0402 cur.beginEditBlock(); 0403 if (selectedText && cur.hasSelection()) { 0404 const int positionStart = qMin(cur.selectionEnd(), cur.selectionStart()); 0405 const int positionEnd = qMax(cur.selectionEnd(), cur.selectionStart()); 0406 cur.setPosition(positionStart); 0407 int cursorPosition = positionStart; 0408 while (cursorPosition < positionEnd) { 0409 if (isLineQuoted(cur.block().text())) { 0410 cur.movePosition(QTextCursor::NextBlock); 0411 } else { 0412 cur.movePosition(QTextCursor::NextWord); 0413 } 0414 cursorPosition = cur.position(); 0415 (void)d->autoCorrection->autocorrect(richText, *document(), cursorPosition); 0416 } 0417 } else { 0418 cur.movePosition(QTextCursor::Start); 0419 while (!cur.atEnd()) { 0420 if (isLineQuoted(cur.block().text())) { 0421 cur.movePosition(QTextCursor::NextBlock); 0422 } else { 0423 cur.movePosition(QTextCursor::NextWord); 0424 } 0425 int cursorPosition = cur.position(); 0426 (void)d->autoCorrection->autocorrect(richText, *document(), cursorPosition); 0427 } 0428 } 0429 cur.endEditBlock(); 0430 if (cur.position() != initialPosition) { 0431 cur.setPosition(initialPosition); 0432 setTextCursor(cur); 0433 } 0434 } 0435 } 0436 0437 #include "moc_richtextcomposerng.cpp"