Messagelib

richtextcomposerng.cpp
1/*
2 SPDX-FileCopyrightText: 2015-2024 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "richtextcomposerng.h"
8#include "richtextcomposersignatures.h"
9#include "settings/messagecomposersettings.h"
10#include <KPIMTextEdit/MarkupDirector>
11#include <KPIMTextEdit/PlainTextMarkupBuilder>
12#include <KPIMTextEdit/RichTextComposerControler>
13#include <KPIMTextEdit/RichTextComposerImages>
14#include <KPIMTextEdit/TextHTMLBuilder>
15
16#include <TextAutoCorrectionCore/AutoCorrection>
17
18#include "part/textpart.h"
19
20#include <KMessageBox>
21
22#include <QRegularExpression>
23
24using namespace MessageComposer;
25
26class MessageComposer::RichTextComposerNgPrivate
27{
28public:
29 explicit RichTextComposerNgPrivate(RichTextComposerNg *q)
30 : richtextComposer(q)
31 , richTextComposerSignatures(new MessageComposer::RichTextComposerSignatures(richtextComposer, richtextComposer))
32 {
33 }
34
35 void fixHtmlFontSize(QString &cleanHtml) const;
36 [[nodiscard]] QString toCleanHtml() const;
37 TextAutoCorrectionCore::AutoCorrection *autoCorrection = nullptr;
38 RichTextComposerNg *const richtextComposer;
39 MessageComposer::RichTextComposerSignatures *const richTextComposerSignatures;
40};
41
42RichTextComposerNg::RichTextComposerNg(QWidget *parent)
43 : KPIMTextEdit::RichTextComposer(parent)
44 , d(new MessageComposer::RichTextComposerNgPrivate(this))
45{
46}
47
48RichTextComposerNg::~RichTextComposerNg() = default;
49
50MessageComposer::RichTextComposerSignatures *RichTextComposerNg::composerSignature() const
51{
52 return d->richTextComposerSignatures;
53}
54
55TextAutoCorrectionCore::AutoCorrection *RichTextComposerNg::autocorrection() const
56{
57 return d->autoCorrection;
58}
59
60void RichTextComposerNg::setAutocorrection(TextAutoCorrectionCore::AutoCorrection *autocorrect)
61{
62 d->autoCorrection = autocorrect;
63}
64
65void RichTextComposerNg::setAutocorrectionLanguage(const QString &lang)
66{
67 if (d->autoCorrection) {
68 TextAutoCorrectionCore::AutoCorrectionSettings *settings = d->autoCorrection->autoCorrectionSettings();
69 settings->setLanguage(lang);
70 d->autoCorrection->setAutoCorrectionSettings(settings);
71 }
72}
73
74static bool isSpecial(const QTextCharFormat &charFormat)
75{
76 return charFormat.isFrameFormat() || charFormat.isImageFormat() || charFormat.isListFormat() || charFormat.isTableFormat()
77 || charFormat.isTableCellFormat();
78}
79
80bool RichTextComposerNg::processModifyText(QKeyEvent *e)
81{
82 if (d->autoCorrection && d->autoCorrection->autoCorrectionSettings()->isEnabledAutoCorrection()) {
83 if ((e->key() == Qt::Key_Space) || (e->key() == Qt::Key_Enter) || (e->key() == Qt::Key_Return)) {
84 if (!isLineQuoted(textCursor().block().text()) && !textCursor().hasSelection()) {
85 const QTextCharFormat initialTextFormat = textCursor().charFormat();
86 const bool richText = (textMode() == RichTextComposer::Rich);
87 int position = textCursor().position();
88 const bool addSpace = d->autoCorrection->autocorrect(richText, *document(), position);
89 QTextCursor cur = textCursor();
90 cur.setPosition(position);
91 const bool spacePressed = (e->key() == Qt::Key_Space);
92 if (overwriteMode() && spacePressed) {
93 if (addSpace) {
94 const QChar insertChar = QLatin1Char(' ');
95 if (!cur.atBlockEnd()) {
97 }
98 if (richText && !isSpecial(initialTextFormat)) {
99 cur.insertText(insertChar, initialTextFormat);
100 } else {
101 cur.insertText(insertChar);
102 }
103 setTextCursor(cur);
104 }
105 } else {
106 const QChar insertChar = spacePressed ? QLatin1Char(' ') : QLatin1Char('\n');
107 if (richText && !isSpecial(initialTextFormat)) {
108 if ((spacePressed && addSpace) || !spacePressed) {
109 cur.insertText(insertChar, initialTextFormat);
110 }
111 } else {
112 if ((spacePressed && addSpace) || !spacePressed) {
113 cur.insertText(insertChar);
114 }
115 }
116 setTextCursor(cur);
117 }
118 return true;
119 }
120 }
121 }
122 return false;
123}
124
125void RichTextComposerNgPrivate::fixHtmlFontSize(QString &cleanHtml) const
126{
127 // non-greedy matching
128 static const QRegularExpression styleRegex(QStringLiteral("<span style=\".*?font-size:(.*?)pt;.*?</span>"));
129
131 int offset = 0;
132 while (cleanHtml.indexOf(styleRegex, offset, &rmatch) != -1) {
133 QString replacement;
134 bool ok = false;
135 const double ptValue = rmatch.captured(1).toDouble(&ok);
136 if (ok) {
137 const double emValue = ptValue / 12;
138 replacement = QString::number(emValue, 'g', 2);
139 const int capLen = rmatch.capturedLength(1);
140 cleanHtml.replace(rmatch.capturedStart(1), capLen + 2 /* QLatin1StringView("pt").size() */, replacement + QLatin1StringView("em"));
141 // advance the offset to just after the last replace
142 offset = rmatch.capturedEnd(0) - capLen + replacement.size();
143 } else {
144 // a match was found but the toDouble call failed, advance the offset to just after
145 // the entire match
146 offset = rmatch.capturedEnd(0);
147 }
148 }
149}
150
151MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus RichTextComposerNg::convertPlainText(MessageComposer::TextPart *textPart)
152{
153 Q_UNUSED(textPart)
154 return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
155}
156
157void RichTextComposerNg::fillComposerTextPart(MessageComposer::TextPart *textPart)
158{
159 const bool wasConverted = convertPlainText(textPart) == MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Converted;
160 if (composerControler()->isFormattingUsed() && !wasConverted) {
161 if (MessageComposer::MessageComposerSettings::self()->improvePlainTextOfHtmlMessage()) {
162 KPIMTextEdit::PlainTextMarkupBuilder pb;
163
165 pmd.processDocument(document());
166 const QString plainText = pb.getResult();
167 textPart->setCleanPlainText(composerControler()->toCleanPlainText(plainText));
169 doc.adjustSize();
170
171 textPart->setWrappedPlainText(composerControler()->toWrappedPlainText(&doc));
172 } else {
173 textPart->setCleanPlainText(composerControler()->toCleanPlainText());
174 textPart->setWrappedPlainText(composerControler()->toWrappedPlainText());
175 }
176 } else if (!wasConverted) {
177 textPart->setCleanPlainText(composerControler()->toCleanPlainText());
178 textPart->setWrappedPlainText(composerControler()->toWrappedPlainText());
179 }
180
181 textPart->setWordWrappingEnabled(lineWrapMode() == QTextEdit::FixedColumnWidth);
182 if (composerControler()->isFormattingUsed() && !wasConverted) {
184
186 pmd.processDocument(document());
187 QString cleanHtml =
188 QStringLiteral("<html>\n<head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n</head>\n<body>%1</body>\n</html>")
189 .arg(pb.getResult());
190 d->fixHtmlFontSize(cleanHtml);
191 textPart->setCleanHtml(cleanHtml);
192 // qDebug() << " cleanHtml grantlee builder" << cleanHtml;
193 // qDebug() << " d->toCleanHtml() " << d->toCleanHtml();
194 textPart->setEmbeddedImages(composerControler()->composerImages()->embeddedImages());
195 }
196}
197
198QString RichTextComposerNgPrivate::toCleanHtml() const
199{
200 QString result = richtextComposer->toHtml();
201
202 static const QString EMPTYLINEHTML = QStringLiteral(
203 "<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; "
204 "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; \">&nbsp;</p>");
205
206 // Qt inserts various style properties based on the current mode of the editor (underline,
207 // bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'.
208 // Minimal/non-greedy matching
209 static const QString EMPTYLINEREGEX = QStringLiteral("<p style=\"-qt-paragraph-type:empty;(?:.*?)</p>");
210
211 static const QString OLLISTPATTERNQT = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
212
213 static const QString ULLISTPATTERNQT = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
214
215 static const QString ORDEREDLISTHTML = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px;");
216
217 static const QString UNORDEREDLISTHTML = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px;");
218
219 // fix 1 - empty lines should show as empty lines - MS Outlook treats margin-top:0px; as
220 // a non-existing line.
221 // Although we can simply remove the margin-top style property, we still get unwanted results
222 // if you have three or more empty lines. It's best to replace empty <p> elements with <p>&nbsp;</p>.
223
224 // Replace all the matching text with the new line text
225 result.replace(QRegularExpression(EMPTYLINEREGEX), EMPTYLINEHTML);
226
227 // fix 2a - ordered lists - MS Outlook treats margin-left:0px; as
228 // a non-existing number; e.g: "1. First item" turns into "First Item"
229 result.replace(OLLISTPATTERNQT, ORDEREDLISTHTML);
230
231 // fix 2b - unordered lists - MS Outlook treats margin-left:0px; as
232 // a non-existing bullet; e.g: "* First bullet" turns into "First Bullet"
233 result.replace(ULLISTPATTERNQT, UNORDEREDLISTHTML);
234
235 return result;
236}
237
238static bool isCursorAtEndOfLine(const QTextCursor &cursor)
239{
240 QTextCursor testCursor = cursor;
242 return !testCursor.hasSelection();
243}
244
245static void insertSignatureHelper(const QString &signature,
246 RichTextComposerNg *textEdit,
248 bool isHtml,
249 bool addNewlines)
250{
251 if (!signature.isEmpty()) {
252 // Save the modified state of the document, as inserting a signature
253 // shouldn't change this. Restore it at the end of this function.
254 bool isModified = textEdit->document()->isModified();
255
256 // Move to the desired position, where the signature should be inserted
257 QTextCursor cursor = textEdit->textCursor();
258 QTextCursor oldCursor = cursor;
259 cursor.beginEditBlock();
260
263 } else if (placement == KIdentityManagementCore::Signature::Start) {
265 } else if (placement == KIdentityManagementCore::Signature::AtCursor) {
267 }
268 textEdit->setTextCursor(cursor);
269
270 QString lineSep;
271 if (addNewlines) {
272 if (isHtml) {
273 lineSep = QStringLiteral("<br>");
274 } else {
275 lineSep = QLatin1Char('\n');
276 }
277 }
278
279 // Insert the signature and newlines depending on where it was inserted.
280 int newCursorPos = -1;
281 QString headSep;
282 QString tailSep;
283
285 // There is one special case when re-setting the old cursor: The cursor
286 // was at the end. In this case, QTextEdit has no way to know
287 // if the signature was added before or after the cursor, and just
288 // decides that it was added before (and the cursor moves to the end,
289 // but it should not when appending a signature). See bug 167961
290 if (oldCursor.position() == textEdit->toPlainText().length()) {
291 newCursorPos = oldCursor.position();
292 }
293 headSep = lineSep;
294 } else if (placement == KIdentityManagementCore::Signature::Start) {
295 // When prepending signatures, add a couple of new lines before
296 // the signature, and move the cursor to the beginning of the QTextEdit.
297 // People tends to insert new text there.
298 newCursorPos = 0;
299 headSep = lineSep + lineSep;
300 if (!isCursorAtEndOfLine(cursor)) {
301 tailSep = lineSep;
302 }
303 } else if (placement == KIdentityManagementCore::Signature::AtCursor) {
304 if (!isCursorAtEndOfLine(cursor)) {
305 tailSep = lineSep;
306 }
307 }
308
309 const QString full_signature = headSep + signature + tailSep;
310 if (isHtml) {
311 textEdit->insertHtml(full_signature);
312 } else {
313 textEdit->insertPlainText(full_signature);
314 }
315
316 cursor.endEditBlock();
317 if (newCursorPos != -1) {
318 oldCursor.setPosition(newCursorPos);
319 }
320
321 textEdit->setTextCursor(oldCursor);
322 textEdit->ensureCursorVisible();
323
324 textEdit->document()->setModified(isModified);
325
326 if (isHtml) {
327 textEdit->activateRichText();
328 }
329 }
330}
331
332void RichTextComposerNg::insertSignature(const KIdentityManagementCore::Signature &signature,
335{
336 if (signature.isEnabledSignature()) {
337 QString signatureStr;
338 bool ok = false;
341 signatureStr = signature.withSeparator(&ok, &errorMessage);
342 } else {
343 signatureStr = signature.rawText(&ok, &errorMessage);
344 }
345
346 if (!ok && !errorMessage.isEmpty()) {
347 KMessageBox::error(nullptr, errorMessage);
348 }
349
350 insertSignatureHelper(signatureStr,
351 this,
352 placement,
353 (signature.isInlinedHtml() && signature.type() == KIdentityManagementCore::Signature::Inlined),
355
356 // We added the text of the signature above, now it is time to add the images as well.
357 if (signature.isInlinedHtml()) {
358 const QList<KIdentityManagementCore::Signature::EmbeddedImagePtr> embeddedImages = signature.embeddedImages();
359 for (const KIdentityManagementCore::Signature::EmbeddedImagePtr &image : embeddedImages) {
360 composerControler()->composerImages()->loadImage(image->image, image->name, image->name);
361 }
362 }
363 }
364}
365
366QString RichTextComposerNg::toCleanHtml() const
367{
368 return d->toCleanHtml();
369}
370
371void RichTextComposerNg::fixHtmlFontSize(QString &cleanHtml) const
372{
373 d->fixHtmlFontSize(cleanHtml);
374}
375
376void RichTextComposerNg::forceAutoCorrection(bool selectedText)
377{
378 if (document()->isEmpty()) {
379 return;
380 }
381 if (d->autoCorrection && d->autoCorrection->autoCorrectionSettings()->isEnabledAutoCorrection()) {
382 const bool richText = (textMode() == RichTextComposer::Rich);
383 const int initialPosition = textCursor().position();
384 QTextCursor cur = textCursor();
385 cur.beginEditBlock();
386 if (selectedText && cur.hasSelection()) {
387 const int positionStart = qMin(cur.selectionEnd(), cur.selectionStart());
388 const int positionEnd = qMax(cur.selectionEnd(), cur.selectionStart());
389 cur.setPosition(positionStart);
390 int cursorPosition = positionStart;
391 while (cursorPosition < positionEnd) {
392 if (isLineQuoted(cur.block().text())) {
394 } else {
396 }
397 cursorPosition = cur.position();
398 (void)d->autoCorrection->autocorrect(richText, *document(), cursorPosition);
399 }
400 } else {
402 while (!cur.atEnd()) {
403 if (isLineQuoted(cur.block().text())) {
405 } else {
407 }
408 int cursorPosition = cur.position();
409 (void)d->autoCorrection->autocorrect(richText, *document(), cursorPosition);
410 }
411 }
412 cur.endEditBlock();
413 if (cur.position() != initialPosition) {
414 cur.setPosition(initialPosition);
415 setTextCursor(cur);
416 }
417 }
418}
419
420#include "moc_richtextcomposerng.cpp"
QString rawText(bool *ok=nullptr, QString *errorMessage=nullptr) const
QString withSeparator(bool *ok=nullptr, QString *errorMessage=nullptr) const
QString getResult() override
The RichTextComposerNg class.
The TextPart class.
Definition textpart.h:21
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
int key() const const
QString captured(QStringView name) const const
qsizetype capturedEnd(QStringView name) const const
qsizetype capturedLength(QStringView name) const const
qsizetype capturedStart(QStringView name) const const
QString arg(Args &&... args) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype length() const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
qsizetype size() const const
double toDouble(bool *ok) const const
Key_Space
QString text() const const
bool atBlockEnd() const const
bool atEnd() const const
void beginEditBlock()
QTextBlock block() const const
QTextCharFormat charFormat() const const
void endEditBlock()
bool hasSelection() const const
void insertText(const QString &text)
bool movePosition(MoveOperation operation, MoveMode mode, int n)
int position() const const
int selectionEnd() const const
int selectionStart() const const
void setPosition(int pos, MoveMode m)
void ensureCursorVisible()
QString toHtml() const const
void insertHtml(const QString &text)
void insertPlainText(const QString &text)
void setTextCursor(const QTextCursor &cursor)
QTextCursor textCursor() const const
QString toPlainText() const const
bool isFrameFormat() const const
bool isImageFormat() const const
bool isListFormat() const const
bool isTableCellFormat() const const
bool isTableFormat() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:33:25 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.