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

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