1/*
2 krichtextedit
3 SPDX-FileCopyrightText: 2007 Laurent Montel <montel@kde.org>
4 SPDX-FileCopyrightText: 2008 Thomas McGuire <thomas.mcguire@gmx.net>
5 SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
6
7 SPDX-License-Identifier: LGPL-2.1-or-later
8*/
9
10#include "krichtextedit.h"
11#include "krichtextedit_p.h"
12
13// Own includes
14#include "klinkdialog_p.h"
15
16// kdelibs includes
17#include <KCursor>
18
19// Qt includes
20#include <QRegularExpression>
21
22void KRichTextEditPrivate::activateRichText()
23{
24 Q_Q(KRichTextEdit);
25
26 if (mMode == KRichTextEdit::Plain) {
27 q->setAcceptRichText(true);
28 mMode = KRichTextEdit::Rich;
29 Q_EMIT q->textModeChanged(mode: mMode);
30 }
31}
32
33void KRichTextEditPrivate::setTextCursor(QTextCursor &cursor)
34{
35 Q_Q(KRichTextEdit);
36
37 q->setTextCursor(cursor);
38}
39
40void KRichTextEditPrivate::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
41{
42 Q_Q(KRichTextEdit);
43
44 QTextCursor cursor = q->textCursor();
45 QTextCursor wordStart(cursor);
46 QTextCursor wordEnd(cursor);
47
48 wordStart.movePosition(op: QTextCursor::StartOfWord);
49 wordEnd.movePosition(op: QTextCursor::EndOfWord);
50
51 cursor.beginEditBlock();
52 if (!cursor.hasSelection() && cursor.position() != wordStart.position() && cursor.position() != wordEnd.position()) {
53 cursor.select(selection: QTextCursor::WordUnderCursor);
54 }
55 cursor.mergeCharFormat(modifier: format);
56 q->mergeCurrentCharFormat(modifier: format);
57 cursor.endEditBlock();
58}
59
60KRichTextEdit::KRichTextEdit(const QString &text, QWidget *parent)
61 : KRichTextEdit(*new KRichTextEditPrivate(this), text, parent)
62{
63}
64
65KRichTextEdit::KRichTextEdit(KRichTextEditPrivate &dd, const QString &text, QWidget *parent)
66 : KTextEdit(dd, text, parent)
67{
68 Q_D(KRichTextEdit);
69
70 d->init();
71}
72
73KRichTextEdit::KRichTextEdit(QWidget *parent)
74 : KRichTextEdit(*new KRichTextEditPrivate(this), parent)
75{
76}
77
78KRichTextEdit::KRichTextEdit(KRichTextEditPrivate &dd, QWidget *parent)
79 : KTextEdit(dd, parent)
80{
81 Q_D(KRichTextEdit);
82
83 d->init();
84}
85
86KRichTextEdit::~KRichTextEdit() = default;
87
88//@cond PRIVATE
89void KRichTextEditPrivate::init()
90{
91 Q_Q(KRichTextEdit);
92
93 q->setAcceptRichText(false);
94 KCursor::setAutoHideCursor(w: q, enable: true, customEventFilter: true);
95}
96//@endcond
97
98void KRichTextEdit::setListStyle(int _styleIndex)
99{
100 Q_D(KRichTextEdit);
101
102 d->nestedListHelper->handleOnBulletType(styleIndex: -_styleIndex);
103 setFocus();
104 d->activateRichText();
105}
106
107void KRichTextEdit::indentListMore()
108{
109 Q_D(KRichTextEdit);
110
111 d->nestedListHelper->changeIndent(delta: +1);
112 d->activateRichText();
113}
114
115void KRichTextEdit::indentListLess()
116{
117 Q_D(KRichTextEdit);
118
119 d->nestedListHelper->changeIndent(delta: -1);
120}
121
122void KRichTextEdit::insertHorizontalRule()
123{
124 Q_D(KRichTextEdit);
125
126 QTextCursor cursor = textCursor();
127 QTextBlockFormat bf = cursor.blockFormat();
128 QTextCharFormat cf = cursor.charFormat();
129
130 cursor.beginEditBlock();
131 cursor.insertHtml(QStringLiteral("<hr>"));
132 cursor.insertBlock(format: bf, charFormat: cf);
133 cursor.endEditBlock();
134 setTextCursor(cursor);
135 d->activateRichText();
136}
137
138void KRichTextEdit::alignLeft()
139{
140 Q_D(KRichTextEdit);
141
142 setAlignment(Qt::AlignLeft);
143 setFocus();
144 d->activateRichText();
145}
146
147void KRichTextEdit::alignCenter()
148{
149 Q_D(KRichTextEdit);
150
151 setAlignment(Qt::AlignHCenter);
152 setFocus();
153 d->activateRichText();
154}
155
156void KRichTextEdit::alignRight()
157{
158 Q_D(KRichTextEdit);
159
160 setAlignment(Qt::AlignRight);
161 setFocus();
162 d->activateRichText();
163}
164
165void KRichTextEdit::alignJustify()
166{
167 Q_D(KRichTextEdit);
168
169 setAlignment(Qt::AlignJustify);
170 setFocus();
171 d->activateRichText();
172}
173
174void KRichTextEdit::makeRightToLeft()
175{
176 Q_D(KRichTextEdit);
177
178 QTextBlockFormat format;
179 format.setLayoutDirection(Qt::RightToLeft);
180 QTextCursor cursor = textCursor();
181 cursor.mergeBlockFormat(modifier: format);
182 setTextCursor(cursor);
183 setFocus();
184 d->activateRichText();
185}
186
187void KRichTextEdit::makeLeftToRight()
188{
189 Q_D(KRichTextEdit);
190
191 QTextBlockFormat format;
192 format.setLayoutDirection(Qt::LeftToRight);
193 QTextCursor cursor = textCursor();
194 cursor.mergeBlockFormat(modifier: format);
195 setTextCursor(cursor);
196 setFocus();
197 d->activateRichText();
198}
199
200void KRichTextEdit::setTextBold(bool bold)
201{
202 Q_D(KRichTextEdit);
203
204 QTextCharFormat fmt;
205 fmt.setFontWeight(bold ? QFont::Bold : QFont::Normal);
206 d->mergeFormatOnWordOrSelection(format: fmt);
207 setFocus();
208 d->activateRichText();
209}
210
211void KRichTextEdit::setTextItalic(bool italic)
212{
213 Q_D(KRichTextEdit);
214
215 QTextCharFormat fmt;
216 fmt.setFontItalic(italic);
217 d->mergeFormatOnWordOrSelection(format: fmt);
218 setFocus();
219 d->activateRichText();
220}
221
222void KRichTextEdit::setTextUnderline(bool underline)
223{
224 Q_D(KRichTextEdit);
225
226 QTextCharFormat fmt;
227 fmt.setFontUnderline(underline);
228 d->mergeFormatOnWordOrSelection(format: fmt);
229 setFocus();
230 d->activateRichText();
231}
232
233void KRichTextEdit::setTextStrikeOut(bool strikeOut)
234{
235 Q_D(KRichTextEdit);
236
237 QTextCharFormat fmt;
238 fmt.setFontStrikeOut(strikeOut);
239 d->mergeFormatOnWordOrSelection(format: fmt);
240 setFocus();
241 d->activateRichText();
242}
243
244void KRichTextEdit::setTextForegroundColor(const QColor &color)
245{
246 Q_D(KRichTextEdit);
247
248 QTextCharFormat fmt;
249 fmt.setForeground(color);
250 d->mergeFormatOnWordOrSelection(format: fmt);
251 setFocus();
252 d->activateRichText();
253}
254
255void KRichTextEdit::setTextBackgroundColor(const QColor &color)
256{
257 Q_D(KRichTextEdit);
258
259 QTextCharFormat fmt;
260 fmt.setBackground(color);
261 d->mergeFormatOnWordOrSelection(format: fmt);
262 setFocus();
263 d->activateRichText();
264}
265
266void KRichTextEdit::setFontFamily(const QString &fontFamily)
267{
268 Q_D(KRichTextEdit);
269
270 QTextCharFormat fmt;
271 fmt.setFontFamilies({fontFamily});
272 d->mergeFormatOnWordOrSelection(format: fmt);
273 setFocus();
274 d->activateRichText();
275}
276
277void KRichTextEdit::setFontSize(int size)
278{
279 Q_D(KRichTextEdit);
280
281 QTextCharFormat fmt;
282 fmt.setFontPointSize(size);
283 d->mergeFormatOnWordOrSelection(format: fmt);
284 setFocus();
285 d->activateRichText();
286}
287
288void KRichTextEdit::setFont(const QFont &font)
289{
290 Q_D(KRichTextEdit);
291
292 QTextCharFormat fmt;
293 fmt.setFont(font);
294 d->mergeFormatOnWordOrSelection(format: fmt);
295 setFocus();
296 d->activateRichText();
297}
298
299void KRichTextEdit::switchToPlainText()
300{
301 Q_D(KRichTextEdit);
302
303 if (d->mMode == Rich) {
304 d->mMode = Plain;
305 // TODO: Warn the user about this?
306 auto insertPlainFunc = [this]() {
307 insertPlainTextImplementation();
308 };
309 QMetaObject::invokeMethod(object: this, function&: insertPlainFunc);
310 setAcceptRichText(false);
311 Q_EMIT textModeChanged(mode: d->mMode);
312 }
313}
314
315void KRichTextEdit::insertPlainTextImplementation()
316{
317 document()->setPlainText(document()->toPlainText());
318}
319
320void KRichTextEdit::setTextSuperScript(bool superscript)
321{
322 Q_D(KRichTextEdit);
323
324 QTextCharFormat fmt;
325 fmt.setVerticalAlignment(superscript ? QTextCharFormat::AlignSuperScript : QTextCharFormat::AlignNormal);
326 d->mergeFormatOnWordOrSelection(format: fmt);
327 setFocus();
328 d->activateRichText();
329}
330
331void KRichTextEdit::setTextSubScript(bool subscript)
332{
333 Q_D(KRichTextEdit);
334
335 QTextCharFormat fmt;
336 fmt.setVerticalAlignment(subscript ? QTextCharFormat::AlignSubScript : QTextCharFormat::AlignNormal);
337 d->mergeFormatOnWordOrSelection(format: fmt);
338 setFocus();
339 d->activateRichText();
340}
341
342void KRichTextEdit::setHeadingLevel(int level)
343{
344 Q_D(KRichTextEdit);
345
346 const int boundedLevel = qBound(min: 0, val: 6, max: level);
347 // Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
348 // level=2 look the same
349 const int sizeAdjustment = boundedLevel > 0 ? 5 - boundedLevel : 0;
350
351 QTextCursor cursor = textCursor();
352 cursor.beginEditBlock();
353
354 QTextBlockFormat blkfmt;
355 blkfmt.setHeadingLevel(boundedLevel);
356 cursor.mergeBlockFormat(modifier: blkfmt);
357
358 QTextCharFormat chrfmt;
359 chrfmt.setFontWeight(boundedLevel > 0 ? QFont::Bold : QFont::Normal);
360 chrfmt.setProperty(propertyId: QTextFormat::FontSizeAdjustment, value: sizeAdjustment);
361 // Applying style to the current line or selection
362 QTextCursor selectCursor = cursor;
363 if (selectCursor.hasSelection()) {
364 QTextCursor top = selectCursor;
365 top.setPosition(pos: qMin(a: top.anchor(), b: top.position()));
366 top.movePosition(op: QTextCursor::StartOfBlock);
367
368 QTextCursor bottom = selectCursor;
369 bottom.setPosition(pos: qMax(a: bottom.anchor(), b: bottom.position()));
370 bottom.movePosition(op: QTextCursor::EndOfBlock);
371
372 selectCursor.setPosition(pos: top.position(), mode: QTextCursor::MoveAnchor);
373 selectCursor.setPosition(pos: bottom.position(), mode: QTextCursor::KeepAnchor);
374 } else {
375 selectCursor.select(selection: QTextCursor::BlockUnderCursor);
376 }
377 selectCursor.mergeCharFormat(modifier: chrfmt);
378
379 cursor.mergeBlockCharFormat(modifier: chrfmt);
380 cursor.endEditBlock();
381 setTextCursor(cursor);
382 setFocus();
383 d->activateRichText();
384}
385
386void KRichTextEdit::enableRichTextMode()
387{
388 Q_D(KRichTextEdit);
389
390 d->activateRichText();
391}
392
393KRichTextEdit::Mode KRichTextEdit::textMode() const
394{
395 Q_D(const KRichTextEdit);
396
397 return d->mMode;
398}
399
400QString KRichTextEdit::textOrHtml() const
401{
402 if (textMode() == Rich) {
403 return toCleanHtml();
404 } else {
405 return toPlainText();
406 }
407}
408
409void KRichTextEdit::setTextOrHtml(const QString &text)
410{
411 Q_D(KRichTextEdit);
412
413 // might be rich text
414 if (Qt::mightBeRichText(text)) {
415 if (d->mMode == KRichTextEdit::Plain) {
416 d->activateRichText();
417 }
418 setHtml(text);
419 } else {
420 setPlainText(text);
421 }
422}
423
424// KF6 TODO: remove constness
425QString KRichTextEdit::currentLinkText() const
426{
427 QTextCursor cursor = textCursor();
428 selectLinkText(cursor: &cursor);
429 return cursor.selectedText();
430}
431
432// KF6 TODO: remove constness
433void KRichTextEdit::selectLinkText() const
434{
435 Q_D(const KRichTextEdit);
436
437 QTextCursor cursor = textCursor();
438 selectLinkText(cursor: &cursor);
439 // KF6 TODO: remove const_cast
440 const_cast<KRichTextEditPrivate *>(d)->setTextCursor(cursor);
441}
442
443void KRichTextEdit::selectLinkText(QTextCursor *cursor) const
444{
445 // If the cursor is on a link, select the text of the link.
446 if (cursor->charFormat().isAnchor()) {
447 QString aHref = cursor->charFormat().anchorHref();
448
449 // Move cursor to start of link
450 while (cursor->charFormat().anchorHref() == aHref) {
451 if (cursor->atStart()) {
452 break;
453 }
454 cursor->setPosition(pos: cursor->position() - 1);
455 }
456 if (cursor->charFormat().anchorHref() != aHref) {
457 cursor->setPosition(pos: cursor->position() + 1, mode: QTextCursor::KeepAnchor);
458 }
459
460 // Move selection to the end of the link
461 while (cursor->charFormat().anchorHref() == aHref) {
462 if (cursor->atEnd()) {
463 break;
464 }
465 cursor->setPosition(pos: cursor->position() + 1, mode: QTextCursor::KeepAnchor);
466 }
467 if (cursor->charFormat().anchorHref() != aHref) {
468 cursor->setPosition(pos: cursor->position() - 1, mode: QTextCursor::KeepAnchor);
469 }
470 } else if (cursor->hasSelection()) {
471 // Nothing to to. Using the currently selected text as the link text.
472 } else {
473 // Select current word
474 cursor->movePosition(op: QTextCursor::StartOfWord);
475 cursor->movePosition(op: QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
476 }
477}
478
479QString KRichTextEdit::currentLinkUrl() const
480{
481 return textCursor().charFormat().anchorHref();
482}
483
484void KRichTextEdit::updateLink(const QString &linkUrl, const QString &linkText)
485{
486 Q_D(KRichTextEdit);
487
488 selectLinkText();
489
490 QTextCursor cursor = textCursor();
491 cursor.beginEditBlock();
492
493 if (!cursor.hasSelection()) {
494 cursor.select(selection: QTextCursor::WordUnderCursor);
495 }
496
497 QTextCharFormat format = cursor.charFormat();
498 // Save original format to create an extra space with the existing char
499 // format for the block
500 const QTextCharFormat originalFormat = format;
501 if (!linkUrl.isEmpty()) {
502 // Add link details
503 format.setAnchor(true);
504 format.setAnchorHref(linkUrl);
505 // Workaround for QTBUG-1814:
506 // Link formatting does not get applied immediately when setAnchor(true)
507 // is called. So the formatting needs to be applied manually.
508 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
509 format.setUnderlineColor(palette().link().color());
510 format.setForeground(palette().link());
511 d->activateRichText();
512 } else {
513 // Remove link details
514 format.setAnchor(false);
515 format.setAnchorHref(QString());
516 // Workaround for QTBUG-1814:
517 // Link formatting does not get removed immediately when setAnchor(false)
518 // is called. So the formatting needs to be applied manually.
519 QTextDocument defaultTextDocument;
520 QTextCharFormat defaultCharFormat = defaultTextDocument.begin().charFormat();
521
522 format.setUnderlineStyle(defaultCharFormat.underlineStyle());
523 format.setUnderlineColor(defaultCharFormat.underlineColor());
524 format.setForeground(defaultCharFormat.foreground());
525 }
526
527 // Insert link text specified in dialog, otherwise write out url.
528 QString _linkText;
529 if (!linkText.isEmpty()) {
530 _linkText = linkText;
531 } else {
532 _linkText = linkUrl;
533 }
534 cursor.insertText(text: _linkText, format);
535
536 // Insert a space after the link if at the end of the block so that
537 // typing some text after the link does not carry link formatting
538 if (!linkUrl.isEmpty() && cursor.atBlockEnd()) {
539 cursor.setPosition(pos: cursor.selectionEnd());
540 cursor.setCharFormat(originalFormat);
541 cursor.insertText(QStringLiteral(" "));
542 }
543
544 cursor.endEditBlock();
545}
546
547void KRichTextEdit::keyPressEvent(QKeyEvent *event)
548{
549 Q_D(KRichTextEdit);
550
551 bool handled = false;
552 if (textCursor().currentList()) {
553 handled = d->nestedListHelper->handleKeyPressEvent(event);
554 }
555
556 // If a line was merged with previous (next) one, with different heading level,
557 // the style should also be adjusted accordingly (i.e. merged)
558 if ((event->key() == Qt::Key_Backspace && textCursor().atBlockStart()
559 && (textCursor().blockFormat().headingLevel() != textCursor().block().previous().blockFormat().headingLevel()))
560 || (event->key() == Qt::Key_Delete && textCursor().atBlockEnd()
561 && (textCursor().blockFormat().headingLevel() != textCursor().block().next().blockFormat().headingLevel()))) {
562 QTextCursor cursor = textCursor();
563 cursor.beginEditBlock();
564 if (event->key() == Qt::Key_Delete) {
565 cursor.deleteChar();
566 } else {
567 cursor.deletePreviousChar();
568 }
569 setHeadingLevel(cursor.blockFormat().headingLevel());
570 cursor.endEditBlock();
571 handled = true;
572 }
573
574 const auto prevHeadingLevel = textCursor().blockFormat().headingLevel();
575 if (!handled) {
576 KTextEdit::keyPressEvent(event);
577 }
578
579 // Match the behavior of office suites: newline after header switches to normal text
580 if (event->key() == Qt::Key_Return //
581 && prevHeadingLevel > 0) {
582 // it should be undoable together with actual "return" keypress
583 textCursor().joinPreviousEditBlock();
584 if (textCursor().atBlockEnd()) {
585 setHeadingLevel(0);
586 } else {
587 setHeadingLevel(prevHeadingLevel);
588 }
589 textCursor().endEditBlock();
590 }
591
592 Q_EMIT cursorPositionChanged();
593}
594
595// void KRichTextEdit::dropEvent(QDropEvent *event)
596// {
597// int dropSize = event->mimeData()->text().size();
598//
599// dropEvent( event );
600// QTextCursor cursor = textCursor();
601// int cursorPosition = cursor.position();
602// cursor.setPosition( cursorPosition - dropSize );
603// cursor.setPosition( cursorPosition, QTextCursor::KeepAnchor );
604// setTextCursor( cursor );
605// d->nestedListHelper->handleAfterDropEvent( event );
606// }
607
608bool KRichTextEdit::canIndentList() const
609{
610 Q_D(const KRichTextEdit);
611
612 return d->nestedListHelper->canIndent();
613}
614
615bool KRichTextEdit::canDedentList() const
616{
617 Q_D(const KRichTextEdit);
618
619 return d->nestedListHelper->canDedent();
620}
621
622QString KRichTextEdit::toCleanHtml() const
623{
624 QString result = toHtml();
625
626 static const QString EMPTYLINEHTML = QLatin1String(
627 "<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; "
628 "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; \">&nbsp;</p>");
629
630 // Qt inserts various style properties based on the current mode of the editor (underline,
631 // bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'.
632 static const QRegularExpression EMPTYLINEREGEX(QStringLiteral("<p style=\"-qt-paragraph-type:empty;(.*?)</p>"));
633
634 static const QString OLLISTPATTERNQT = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
635
636 static const QString ULLISTPATTERNQT = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
637
638 static const QString ORDEREDLISTHTML = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px;");
639
640 static const QString UNORDEREDLISTHTML = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px;");
641
642 // fix 1 - empty lines should show as empty lines - MS Outlook treats margin-top:0px; as
643 // a non-existing line.
644 // Although we can simply remove the margin-top style property, we still get unwanted results
645 // if you have three or more empty lines. It's best to replace empty <p> elements with <p>&nbsp;</p>.
646 // replace all occurrences with the new line text
647 result.replace(re: EMPTYLINEREGEX, after: EMPTYLINEHTML);
648
649 // fix 2a - ordered lists - MS Outlook treats margin-left:0px; as
650 // a non-existing number; e.g: "1. First item" turns into "First Item"
651 result.replace(before: OLLISTPATTERNQT, after: ORDEREDLISTHTML);
652
653 // fix 2b - unordered lists - MS Outlook treats margin-left:0px; as
654 // a non-existing bullet; e.g: "* First bullet" turns into "First Bullet"
655 result.replace(before: ULLISTPATTERNQT, after: UNORDEREDLISTHTML);
656
657 return result;
658}
659
660#include "moc_krichtextedit.cpp"
661

source code of ktextwidgets/src/widgets/krichtextedit.cpp