Messagelib

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

KDE's Doxygen guidelines are available online.