Messagelib

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

KDE's Doxygen guidelines are available online.