Messagelib

richtextcomposerng.cpp
1 /*
2  SPDX-FileCopyrightText: 2015-2020 Laurent Montel <[email protected]>
3 
4  SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "richtextcomposerng.h"
8 #include <KPIMTextEdit/RichTextComposerControler>
9 #include <KPIMTextEdit/RichTextComposerImages>
10 #include "richtextcomposersignatures.h"
11 #include <PimCommon/AutoCorrection>
12 #include <part/textpart.h>
13 #include "settings/messagecomposersettings.h"
14 #include <KPIMTextEdit/TextHTMLBuilder>
15 #include <KPIMTextEdit/MarkupDirector>
16 #include <KPIMTextEdit/PlainTextMarkupBuilder>
17 
18 #define USE_TEXTHTML_BUILDER 1
19 
20 using namespace MessageComposer;
21 
22 class MessageComposer::RichTextComposerNgPrivate
23 {
24 public:
25  RichTextComposerNgPrivate(RichTextComposerNg *q)
26  : richtextComposer(q)
27  {
28  richTextComposerSignatures = new MessageComposer::RichTextComposerSignatures(richtextComposer, richtextComposer);
29  }
30 
31  void fixHtmlFontSize(QString &cleanHtml);
32  Q_REQUIRED_RESULT QString toCleanHtml() const;
33  PimCommon::AutoCorrection *autoCorrection = nullptr;
34  RichTextComposerNg * const richtextComposer;
35  MessageComposer::RichTextComposerSignatures *richTextComposerSignatures = nullptr;
36 };
37 
38 RichTextComposerNg::RichTextComposerNg(QWidget *parent)
39  : KPIMTextEdit::RichTextComposer(parent)
40  , d(new MessageComposer::RichTextComposerNgPrivate(this))
41 {
42 }
43 
44 RichTextComposerNg::~RichTextComposerNg()
45 {
46  delete d;
47 }
48 
49 MessageComposer::RichTextComposerSignatures *RichTextComposerNg::composerSignature() const
50 {
51  return d->richTextComposerSignatures;
52 }
53 
54 PimCommon::AutoCorrection *RichTextComposerNg::autocorrection() const
55 {
56  return d->autoCorrection;
57 }
58 
59 void RichTextComposerNg::setAutocorrection(PimCommon::AutoCorrection *autocorrect)
60 {
61  d->autoCorrection = autocorrect;
62 }
63 
64 void RichTextComposerNg::setAutocorrectionLanguage(const QString &lang)
65 {
66  if (d->autoCorrection) {
67  d->autoCorrection->setLanguage(lang);
68  }
69 }
70 
71 static bool isSpecial(const QTextCharFormat &charFormat)
72 {
73  return charFormat.isFrameFormat() || charFormat.isImageFormat()
74  || charFormat.isListFormat() || charFormat.isTableFormat() || charFormat.isTableCellFormat();
75 }
76 
77 bool RichTextComposerNg::processModifyText(QKeyEvent *e)
78 {
79  if (d->autoCorrection && d->autoCorrection->isEnabledAutoCorrection()) {
80  if ((e->key() == Qt::Key_Space) || (e->key() == Qt::Key_Enter) || (e->key() == Qt::Key_Return)) {
81  if (!isLineQuoted(textCursor().block().text()) && !textCursor().hasSelection()) {
82  const QTextCharFormat initialTextFormat = textCursor().charFormat();
83  const bool richText = (textMode() == RichTextComposer::Rich);
84  int position = textCursor().position();
85  const bool addSpace = d->autoCorrection->autocorrect(richText, *document(), position);
86  QTextCursor cur = textCursor();
87  cur.setPosition(position);
88  const bool spacePressed = (e->key() == Qt::Key_Space);
89  if (overwriteMode() && spacePressed) {
90  if (addSpace) {
91  const QChar insertChar = QLatin1Char(' ');
92  if (!cur.atBlockEnd()) {
94  }
95  if (richText && !isSpecial(initialTextFormat)) {
96  cur.insertText(insertChar, initialTextFormat);
97  } else {
98  cur.insertText(insertChar);
99  }
100  setTextCursor(cur);
101  }
102  } else {
103  const QChar insertChar = spacePressed ? QLatin1Char(' ') : QLatin1Char('\n');
104  if (richText && !isSpecial(initialTextFormat)) {
105  if ((spacePressed && addSpace) || !spacePressed) {
106  cur.insertText(insertChar, initialTextFormat);
107  }
108  } else {
109  if ((spacePressed && addSpace) || !spacePressed) {
110  cur.insertText(insertChar);
111  }
112  }
113  setTextCursor(cur);
114  }
115  return true;
116  }
117  }
118  }
119  return false;
120 }
121 
122 void RichTextComposerNgPrivate::fixHtmlFontSize(QString &cleanHtml)
123 {
124  static const QString FONTSTYLEREGEX = QStringLiteral("<span style=\".*font-size:(.*)pt;.*</span>");
125  QRegExp styleRegex(FONTSTYLEREGEX);
126  styleRegex.setMinimal(true);
127 
128  int offset = styleRegex.indexIn(cleanHtml, 0);
129  while (offset != -1) {
130  // replace all the matching text with the new line text
131  bool ok = false;
132  const QString fontSizeStr = styleRegex.cap(1);
133  const int ptValue = fontSizeStr.toInt(&ok);
134  if (ok) {
135  double emValue = static_cast<double>(ptValue) / 12;
136  const QString emValueStr = QString::number(emValue, 'g', 2);
137  cleanHtml.replace(styleRegex.pos(1), QString(fontSizeStr + QLatin1String("px")).length(), emValueStr + QLatin1String("em"));
138  }
139  // advance the search offset to just beyond the last replace
140  offset += styleRegex.matchedLength();
141  // find the next occurrence
142  offset = styleRegex.indexIn(cleanHtml, offset);
143  }
144 }
145 
146 MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus RichTextComposerNg::convertPlainText(MessageComposer::TextPart *textPart)
147 {
148  Q_UNUSED(textPart);
149  return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted;
150 }
151 
152 void RichTextComposerNg::fillComposerTextPart(MessageComposer::TextPart *textPart)
153 {
154  const bool wasConverted
155  = convertPlainText(textPart) == MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Converted;
156  if (composerControler()->isFormattingUsed()) {
157  if (!wasConverted) {
158  if (MessageComposer::MessageComposerSettings::self()->improvePlainTextOfHtmlMessage()) {
159  KPIMTextEdit::PlainTextMarkupBuilder *pb = new KPIMTextEdit::PlainTextMarkupBuilder();
160 
162  pmd->processDocument(document());
163  const QString plainText = pb->getResult();
164  textPart->setCleanPlainText(composerControler()->toCleanPlainText(plainText));
165  QTextDocument *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
187 
189  pmd->processDocument(document());
190  QString cleanHtml = QStringLiteral("<html>\n<head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n</head>\n<body>%1</body>\n</html>").arg(pb->getResult());
191  delete pmd;
192  delete pb;
193  d->fixHtmlFontSize(cleanHtml);
194  textPart->setCleanHtml(cleanHtml);
195  //qDebug() << " cleanHtml grantlee builder" << cleanHtml;
196 #else
197  QString cleanHtml = d->toCleanHtml();
198  d->fixHtmlFontSize(cleanHtml);
199  textPart->setCleanHtml(cleanHtml);
200  qDebug() << "cleanHtml " << cleanHtml;
201 #endif
202  textPart->setEmbeddedImages(composerControler()->composerImages()->embeddedImages());
203  }
204 }
205 
206 QString RichTextComposerNgPrivate::toCleanHtml() const
207 {
208  QString result = richtextComposer->toHtml();
209 
210  static const QString EMPTYLINEHTML = QStringLiteral(
211  "<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; "
212  "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; \">&nbsp;</p>");
213 
214  // Qt inserts various style properties based on the current mode of the editor (underline,
215  // bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'.
216  static const QString EMPTYLINEREGEX = QStringLiteral(
217  "<p style=\"-qt-paragraph-type:empty;(.*)</p>");
218 
219  static const QString OLLISTPATTERNQT = QStringLiteral(
220  "<ol style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
221 
222  static const QString ULLISTPATTERNQT = QStringLiteral(
223  "<ul style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
224 
225  static const QString ORDEREDLISTHTML = QStringLiteral(
226  "<ol style=\"margin-top: 0px; margin-bottom: 0px;");
227 
228  static const QString UNORDEREDLISTHTML = QStringLiteral(
229  "<ul style=\"margin-top: 0px; margin-bottom: 0px;");
230 
231  // fix 1 - empty lines should show as empty lines - MS Outlook treats margin-top:0px; as
232  // a non-existing line.
233  // Although we can simply remove the margin-top style property, we still get unwanted results
234  // if you have three or more empty lines. It's best to replace empty <p> elements with <p>&nbsp;</p>.
235 
236  QRegExp emptyLineFinder(EMPTYLINEREGEX);
237  emptyLineFinder.setMinimal(true);
238 
239  // find the first occurrence
240  int offset = emptyLineFinder.indexIn(result, 0);
241  while (offset != -1) {
242  // replace all the matching text with the new line text
243  result.replace(offset, emptyLineFinder.matchedLength(), EMPTYLINEHTML);
244  // advance the search offset to just beyond the last replace
245  offset += EMPTYLINEHTML.length();
246  // find the next occurrence
247  offset = emptyLineFinder.indexIn(result, offset);
248  }
249 
250  // fix 2a - ordered lists - MS Outlook treats margin-left:0px; as
251  // a non-existing number; e.g: "1. First item" turns into "First Item"
252  result.replace(OLLISTPATTERNQT, ORDEREDLISTHTML);
253 
254  // fix 2b - unordered lists - MS Outlook treats margin-left:0px; as
255  // a non-existing bullet; e.g: "* First bullet" turns into "First Bullet"
256  result.replace(ULLISTPATTERNQT, UNORDEREDLISTHTML);
257 
258  return result;
259 }
260 
261 static bool isCursorAtEndOfLine(const QTextCursor &cursor)
262 {
263  QTextCursor testCursor = cursor;
265  return !testCursor.hasSelection();
266 }
267 
268 static void insertSignatureHelper(const QString &signature, RichTextComposerNg *textEdit, KIdentityManagement::Signature::Placement placement, bool isHtml, bool addNewlines)
269 {
270  if (!signature.isEmpty()) {
271  // Save the modified state of the document, as inserting a signature
272  // shouldn't change this. Restore it at the end of this function.
273  bool isModified = textEdit->document()->isModified();
274 
275  // Move to the desired position, where the signature should be inserted
276  QTextCursor cursor = textEdit->textCursor();
277  QTextCursor oldCursor = cursor;
278  cursor.beginEditBlock();
279 
280  if (placement == KIdentityManagement::Signature::End) {
282  } else if (placement == KIdentityManagement::Signature::Start) {
284  } else if (placement == KIdentityManagement::Signature::AtCursor) {
286  }
287  textEdit->setTextCursor(cursor);
288 
289  QString lineSep;
290  if (addNewlines) {
291  if (isHtml) {
292  lineSep = QStringLiteral("<br>");
293  } else {
294  lineSep = QLatin1Char('\n');
295  }
296  }
297 
298  // Insert the signature and newlines depending on where it was inserted.
299  int newCursorPos = -1;
300  QString headSep;
301  QString tailSep;
302 
303  if (placement == KIdentityManagement::Signature::End) {
304  // There is one special case when re-setting the old cursor: The cursor
305  // was at the end. In this case, QTextEdit has no way to know
306  // if the signature was added before or after the cursor, and just
307  // decides that it was added before (and the cursor moves to the end,
308  // but it should not when appending a signature). See bug 167961
309  if (oldCursor.position() == textEdit->toPlainText().length()) {
310  newCursorPos = oldCursor.position();
311  }
312  headSep = lineSep;
313  } else if (placement == KIdentityManagement::Signature::Start) {
314  // When prepending signatures, add a couple of new lines before
315  // the signature, and move the cursor to the beginning of the QTextEdit.
316  // People tends to insert new text there.
317  newCursorPos = 0;
318  headSep = lineSep + lineSep;
319  if (!isCursorAtEndOfLine(cursor)) {
320  tailSep = lineSep;
321  }
322  } else if (placement == KIdentityManagement::Signature::AtCursor) {
323  if (!isCursorAtEndOfLine(cursor)) {
324  tailSep = lineSep;
325  }
326  }
327 
328  const QString full_signature = headSep + signature + tailSep;
329  if (isHtml) {
330  textEdit->insertHtml(full_signature);
331  } else {
332  textEdit->insertPlainText(full_signature);
333  }
334 
335  cursor.endEditBlock();
336  if (newCursorPos != -1) {
337  oldCursor.setPosition(newCursorPos);
338  }
339 
340  textEdit->setTextCursor(oldCursor);
341  textEdit->ensureCursorVisible();
342 
343  textEdit->document()->setModified(isModified);
344 
345  if (isHtml) {
346  textEdit->activateRichText();
347  }
348  }
349 }
350 
351 void RichTextComposerNg::insertSignature(const KIdentityManagement::Signature &signature, KIdentityManagement::Signature::Placement placement, KIdentityManagement::Signature::AddedText addedText)
352 {
353  if (signature.isEnabledSignature()) {
354  QString signatureStr;
356  signatureStr = signature.withSeparator();
357  } else {
358  signatureStr = signature.rawText();
359  }
360 
361  insertSignatureHelper(signatureStr, this, placement,
362  (signature.isInlinedHtml()
363  && signature.type() == KIdentityManagement::Signature::Inlined),
365 
366  // We added the text of the signature above, now it is time to add the images as well.
367  if (signature.isInlinedHtml()) {
368  const QVector<KIdentityManagement::Signature::EmbeddedImagePtr> embeddedImages = signature.embeddedImages();
369  for (const KIdentityManagement::Signature::EmbeddedImagePtr &image : embeddedImages) {
370  composerControler()->composerImages()->loadImage(image->image, image->name, image->name);
371  }
372  }
373  }
374 }
375 
376 QString RichTextComposerNg::toCleanHtml() const
377 {
378  return d->toCleanHtml();
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.
The RichTextComposerSignatures class.
void insertHtml(const QString &text)
int selectionStart() const const
QString withSeparator(bool *ok=nullptr) const
QString rawText(bool *ok=nullptr) const
bool movePosition(QTextCursor::MoveOperation operation, QTextCursor::MoveMode mode, int n)
bool hasSelection() const const
QString getResult() override
QString number(int n, int base)
int toInt(bool *ok, int base) const const
void endEditBlock()
void insertText(const QString &text)
bool isEmpty() const const
QString toPlainText() const const
bool isTableCellFormat() const const
void setTextCursor(const QTextCursor &cursor)
virtual void processDocument(QTextDocument *doc)
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()
void adjustSize()
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
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-2020 The KDE developers.
Generated on Sun Aug 2 2020 23:12:02 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.