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

KDE's Doxygen guidelines are available online.