Messagelib

richtextcomposerng.cpp
1 /*
2  SPDX-FileCopyrightText: 2015-2023 Laurent Montel <[email protected]>
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 #include <TextAutoCorrection/AutoCorrection>
16 #include <part/textpart.h>
17 
18 #include <KMessageBox>
19 
20 #include <QRegularExpression>
21 
22 #define USE_TEXTHTML_BUILDER 1
23 
24 using namespace MessageComposer;
25 
26 class MessageComposer::RichTextComposerNgPrivate
27 {
28 public:
29  explicit RichTextComposerNgPrivate(RichTextComposerNg *q)
30  : richtextComposer(q)
31  {
32  richTextComposerSignatures = new MessageComposer::RichTextComposerSignatures(richtextComposer, richtextComposer);
33  }
34 
35  void fixHtmlFontSize(QString &cleanHtml) const;
36  Q_REQUIRED_RESULT QString toCleanHtml() const;
37  TextAutoCorrection::AutoCorrection *autoCorrection = nullptr;
38  RichTextComposerNg *const richtextComposer;
39  MessageComposer::RichTextComposerSignatures *richTextComposerSignatures = nullptr;
40 };
41 
42 RichTextComposerNg::RichTextComposerNg(QWidget *parent)
43  : KPIMTextEdit::RichTextComposer(parent)
44  , d(new MessageComposer::RichTextComposerNgPrivate(this))
45 {
46 }
47 
48 RichTextComposerNg::~RichTextComposerNg() = default;
49 
50 MessageComposer::RichTextComposerSignatures *RichTextComposerNg::composerSignature() const
51 {
52  return d->richTextComposerSignatures;
53 }
54 
55 TextAutoCorrection::AutoCorrection *RichTextComposerNg::autocorrection() const
56 {
57  return d->autoCorrection;
58 }
59 
60 void RichTextComposerNg::setAutocorrection(TextAutoCorrection::AutoCorrection *autocorrect)
61 {
62  d->autoCorrection = autocorrect;
63 }
64 
65 void RichTextComposerNg::setAutocorrectionLanguage(const QString &lang)
66 {
67  if (d->autoCorrection) {
68  TextAutoCorrection::AutoCorrectionSettings *settings = d->autoCorrection->autoCorrectionSettings();
69  settings->setLanguage(lang);
70  d->autoCorrection->setAutoCorrectionSettings(settings);
71  }
72 }
73 
74 static bool isSpecial(const QTextCharFormat &charFormat)
75 {
76  return charFormat.isFrameFormat() || charFormat.isImageFormat() || charFormat.isListFormat() || charFormat.isTableFormat()
77  || charFormat.isTableCellFormat();
78 }
79 
80 bool 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 
125 void 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 /* QLatin1String("pt").size() */, replacement + QLatin1String("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 
151 MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus RichTextComposerNg::convertPlainText(MessageComposer::TextPart *textPart)
152 {
153  Q_UNUSED(textPart)
154  return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
155 }
156 
157 void RichTextComposerNg::fillComposerTextPart(MessageComposer::TextPart *textPart)
158 {
159  const bool wasConverted = convertPlainText(textPart) == MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Converted;
160  if (composerControler()->isFormattingUsed()) {
161  if (!wasConverted) {
162  if (MessageComposer::MessageComposerSettings::self()->improvePlainTextOfHtmlMessage()) {
163  auto pb = new KPIMTextEdit::PlainTextMarkupBuilder();
164 
165  auto pmd = new KPIMTextEdit::MarkupDirector(pb);
166  pmd->processDocument(document());
167  const QString plainText = pb->getResult();
168  textPart->setCleanPlainText(composerControler()->toCleanPlainText(plainText));
169  auto doc = new QTextDocument(plainText);
170  doc->adjustSize();
171 
172  textPart->setWrappedPlainText(composerControler()->toWrappedPlainText(doc));
173  delete doc;
174  delete pmd;
175  delete pb;
176  } else {
177  textPart->setCleanPlainText(composerControler()->toCleanPlainText());
178  textPart->setWrappedPlainText(composerControler()->toWrappedPlainText());
179  }
180  }
181  } else {
182  if (!wasConverted) {
183  textPart->setCleanPlainText(composerControler()->toCleanPlainText());
184  textPart->setWrappedPlainText(composerControler()->toWrappedPlainText());
185  }
186  }
187  textPart->setWordWrappingEnabled(lineWrapMode() == QTextEdit::FixedColumnWidth);
188  if (composerControler()->isFormattingUsed() && !wasConverted) {
189 #ifdef USE_TEXTHTML_BUILDER
190  auto pb = new KPIMTextEdit::TextHTMLBuilder();
191 
192  auto pmd = new KPIMTextEdit::MarkupDirector(pb);
193  pmd->processDocument(document());
194  QString cleanHtml =
195  QStringLiteral("<html>\n<head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n</head>\n<body>%1</body>\n</html>")
196  .arg(pb->getResult());
197  delete pmd;
198  delete pb;
199  d->fixHtmlFontSize(cleanHtml);
200  textPart->setCleanHtml(cleanHtml);
201  // qDebug() << " cleanHtml grantlee builder" << cleanHtml;
202  // qDebug() << " d->toCleanHtml() " << d->toCleanHtml();
203 #else
204  QString cleanHtml = d->toCleanHtml();
205  d->fixHtmlFontSize(cleanHtml);
206  textPart->setCleanHtml(cleanHtml);
207  qDebug() << "cleanHtml " << cleanHtml;
208 #endif
209  textPart->setEmbeddedImages(composerControler()->composerImages()->embeddedImages());
210  }
211 }
212 
213 QString RichTextComposerNgPrivate::toCleanHtml() const
214 {
215  QString result = richtextComposer->toHtml();
216 
217  static const QString EMPTYLINEHTML = QStringLiteral(
218  "<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; "
219  "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; \">&nbsp;</p>");
220 
221  // Qt inserts various style properties based on the current mode of the editor (underline,
222  // bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'.
223  // Minimal/non-greedy matching
224  static const QString EMPTYLINEREGEX = QStringLiteral("<p style=\"-qt-paragraph-type:empty;(?:.*?)</p>");
225 
226  static const QString OLLISTPATTERNQT = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
227 
228  static const QString ULLISTPATTERNQT = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
229 
230  static const QString ORDEREDLISTHTML = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px;");
231 
232  static const QString UNORDEREDLISTHTML = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px;");
233 
234  // fix 1 - empty lines should show as empty lines - MS Outlook treats margin-top:0px; as
235  // a non-existing line.
236  // Although we can simply remove the margin-top style property, we still get unwanted results
237  // if you have three or more empty lines. It's best to replace empty <p> elements with <p>&nbsp;</p>.
238 
239  // Replace all the matching text with the new line text
240  result.replace(QRegularExpression(EMPTYLINEREGEX), EMPTYLINEHTML);
241 
242  // fix 2a - ordered lists - MS Outlook treats margin-left:0px; as
243  // a non-existing number; e.g: "1. First item" turns into "First Item"
244  result.replace(OLLISTPATTERNQT, ORDEREDLISTHTML);
245 
246  // fix 2b - unordered lists - MS Outlook treats margin-left:0px; as
247  // a non-existing bullet; e.g: "* First bullet" turns into "First Bullet"
248  result.replace(ULLISTPATTERNQT, UNORDEREDLISTHTML);
249 
250  return result;
251 }
252 
253 static bool isCursorAtEndOfLine(const QTextCursor &cursor)
254 {
255  QTextCursor testCursor = cursor;
257  return !testCursor.hasSelection();
258 }
259 
260 static void insertSignatureHelper(const QString &signature,
261  RichTextComposerNg *textEdit,
263  bool isHtml,
264  bool addNewlines)
265 {
266  if (!signature.isEmpty()) {
267  // Save the modified state of the document, as inserting a signature
268  // shouldn't change this. Restore it at the end of this function.
269  bool isModified = textEdit->document()->isModified();
270 
271  // Move to the desired position, where the signature should be inserted
272  QTextCursor cursor = textEdit->textCursor();
273  QTextCursor oldCursor = cursor;
274  cursor.beginEditBlock();
275 
276  if (placement == KIdentityManagement::Signature::End) {
278  } else if (placement == KIdentityManagement::Signature::Start) {
280  } else if (placement == KIdentityManagement::Signature::AtCursor) {
282  }
283  textEdit->setTextCursor(cursor);
284 
285  QString lineSep;
286  if (addNewlines) {
287  if (isHtml) {
288  lineSep = QStringLiteral("<br>");
289  } else {
290  lineSep = QLatin1Char('\n');
291  }
292  }
293 
294  // Insert the signature and newlines depending on where it was inserted.
295  int newCursorPos = -1;
296  QString headSep;
297  QString tailSep;
298 
299  if (placement == KIdentityManagement::Signature::End) {
300  // There is one special case when re-setting the old cursor: The cursor
301  // was at the end. In this case, QTextEdit has no way to know
302  // if the signature was added before or after the cursor, and just
303  // decides that it was added before (and the cursor moves to the end,
304  // but it should not when appending a signature). See bug 167961
305  if (oldCursor.position() == textEdit->toPlainText().length()) {
306  newCursorPos = oldCursor.position();
307  }
308  headSep = lineSep;
309  } else if (placement == KIdentityManagement::Signature::Start) {
310  // When prepending signatures, add a couple of new lines before
311  // the signature, and move the cursor to the beginning of the QTextEdit.
312  // People tends to insert new text there.
313  newCursorPos = 0;
314  headSep = lineSep + lineSep;
315  if (!isCursorAtEndOfLine(cursor)) {
316  tailSep = lineSep;
317  }
318  } else if (placement == KIdentityManagement::Signature::AtCursor) {
319  if (!isCursorAtEndOfLine(cursor)) {
320  tailSep = lineSep;
321  }
322  }
323 
324  const QString full_signature = headSep + signature + tailSep;
325  if (isHtml) {
326  textEdit->insertHtml(full_signature);
327  } else {
328  textEdit->insertPlainText(full_signature);
329  }
330 
331  cursor.endEditBlock();
332  if (newCursorPos != -1) {
333  oldCursor.setPosition(newCursorPos);
334  }
335 
336  textEdit->setTextCursor(oldCursor);
337  textEdit->ensureCursorVisible();
338 
339  textEdit->document()->setModified(isModified);
340 
341  if (isHtml) {
342  textEdit->activateRichText();
343  }
344  }
345 }
346 
347 void RichTextComposerNg::insertSignature(const KIdentityManagement::Signature &signature,
350 {
351  if (signature.isEnabledSignature()) {
352  QString signatureStr;
353  bool ok = false;
356  signatureStr = signature.withSeparator(&ok, &errorMessage);
357  } else {
358  signatureStr = signature.rawText(&ok, &errorMessage);
359  }
360 
361  if (!ok && !errorMessage.isEmpty()) {
362  KMessageBox::error(nullptr, errorMessage);
363  }
364 
365  insertSignatureHelper(signatureStr,
366  this,
367  placement,
368  (signature.isInlinedHtml() && signature.type() == KIdentityManagement::Signature::Inlined),
370 
371  // We added the text of the signature above, now it is time to add the images as well.
372  if (signature.isInlinedHtml()) {
373  const QVector<KIdentityManagement::Signature::EmbeddedImagePtr> embeddedImages = signature.embeddedImages();
374  for (const KIdentityManagement::Signature::EmbeddedImagePtr &image : embeddedImages) {
375  composerControler()->composerImages()->loadImage(image->image, image->name, image->name);
376  }
377  }
378  }
379 }
380 
381 QString RichTextComposerNg::toCleanHtml() const
382 {
383  return d->toCleanHtml();
384 }
385 
386 void RichTextComposerNg::fixHtmlFontSize(QString &cleanHtml) const
387 {
388  d->fixHtmlFontSize(cleanHtml);
389 }
390 
391 void RichTextComposerNg::forceAutoCorrection(bool selectedText)
392 {
393  if (document()->isEmpty()) {
394  return;
395  }
396  if (d->autoCorrection && d->autoCorrection->autoCorrectionSettings()->isEnabledAutoCorrection()) {
397  const bool richText = (textMode() == RichTextComposer::Rich);
398  const int initialPosition = textCursor().position();
399  QTextCursor cur = textCursor();
400  cur.beginEditBlock();
401  if (selectedText && cur.hasSelection()) {
402  const int positionStart = qMin(cur.selectionEnd(), cur.selectionStart());
403  const int positionEnd = qMax(cur.selectionEnd(), cur.selectionStart());
404  cur.setPosition(positionStart);
405  int cursorPosition = positionStart;
406  while (cursorPosition < positionEnd) {
407  if (isLineQuoted(cur.block().text())) {
409  } else {
411  }
412  cursorPosition = cur.position();
413  (void)d->autoCorrection->autocorrect(richText, *document(), cursorPosition);
414  }
415  } else {
417  while (!cur.atEnd()) {
418  if (isLineQuoted(cur.block().text())) {
420  } else {
422  }
423  int cursorPosition = cur.position();
424  (void)d->autoCorrection->autocorrect(richText, *document(), cursorPosition);
425  }
426  }
427  cur.endEditBlock();
428  if (cur.position() != initialPosition) {
429  cur.setPosition(initialPosition);
430  setTextCursor(cur);
431  }
432  }
433 }
QString toPlainText() const const
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
QString number(int n, int base)
QTextCursor textCursor() const const
void insertHtml(const QString &text)
int size() const const
int selectionStart() const const
void insertPlainText(const QString &text)
QString text() const const
void setTextCursor(const QTextCursor &cursor)
QTextBlock block() const const
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
bool atBlockEnd() const const
bool isEmpty() const const
int length() const const
bool isImageFormat() const const
bool isListFormat() const const
bool atEnd() const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
Key_Space
int selectionEnd() const const
The RichTextComposerNg class.
double toDouble(bool *ok) const const
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QString & replace(int position, int n, QChar after)
bool isTableFormat() const const
The TextPart class.
Definition: textpart.h:20
void setPosition(int pos, QTextCursor::MoveMode m)
void beginEditBlock()
QString withSeparator(bool *ok=nullptr, QString *errorMessage=nullptr) const
int key() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
int capturedEnd(int nth) const const
void insertText(const QString &text)
bool hasSelection() const const
void endEditBlock()
bool movePosition(QTextCursor::MoveOperation operation, QTextCursor::MoveMode mode, int n)
bool isFrameFormat() const const
bool isTableCellFormat() const const
QString rawText(bool *ok=nullptr, QString *errorMessage=nullptr) const
int position() const const
QString captured(int nth) const const
int capturedLength(int nth) const const
int capturedStart(int nth) const const
void ensureCursorVisible()
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Wed Mar 22 2023 04:07:15 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.