File indexing completed on 2024-04-21 03:58:27

0001 /*
0002     krichtextedit
0003     SPDX-FileCopyrightText: 2007 Laurent Montel <montel@kde.org>
0004     SPDX-FileCopyrightText: 2008 Thomas McGuire <thomas.mcguire@gmx.net>
0005     SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
0006 
0007     SPDX-License-Identifier: LGPL-2.1-or-later
0008 */
0009 
0010 #include "krichtextedit.h"
0011 #include "krichtextedit_p.h"
0012 
0013 // Own includes
0014 #include "klinkdialog_p.h"
0015 
0016 // kdelibs includes
0017 #include <KColorScheme>
0018 #include <KCursor>
0019 
0020 // Qt includes
0021 #include <QRegularExpression>
0022 
0023 void KRichTextEditPrivate::activateRichText()
0024 {
0025     Q_Q(KRichTextEdit);
0026 
0027     if (mMode == KRichTextEdit::Plain) {
0028         q->setAcceptRichText(true);
0029         mMode = KRichTextEdit::Rich;
0030         Q_EMIT q->textModeChanged(mMode);
0031     }
0032 }
0033 
0034 void KRichTextEditPrivate::setTextCursor(QTextCursor &cursor)
0035 {
0036     Q_Q(KRichTextEdit);
0037 
0038     q->setTextCursor(cursor);
0039 }
0040 
0041 void KRichTextEditPrivate::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
0042 {
0043     Q_Q(KRichTextEdit);
0044 
0045     QTextCursor cursor = q->textCursor();
0046     QTextCursor wordStart(cursor);
0047     QTextCursor wordEnd(cursor);
0048 
0049     wordStart.movePosition(QTextCursor::StartOfWord);
0050     wordEnd.movePosition(QTextCursor::EndOfWord);
0051 
0052     cursor.beginEditBlock();
0053     if (!cursor.hasSelection() && cursor.position() != wordStart.position() && cursor.position() != wordEnd.position()) {
0054         cursor.select(QTextCursor::WordUnderCursor);
0055     }
0056     cursor.mergeCharFormat(format);
0057     q->mergeCurrentCharFormat(format);
0058     cursor.endEditBlock();
0059 }
0060 
0061 KRichTextEdit::KRichTextEdit(const QString &text, QWidget *parent)
0062     : KRichTextEdit(*new KRichTextEditPrivate(this), text, parent)
0063 {
0064 }
0065 
0066 KRichTextEdit::KRichTextEdit(KRichTextEditPrivate &dd, const QString &text, QWidget *parent)
0067     : KTextEdit(dd, text, parent)
0068 {
0069     Q_D(KRichTextEdit);
0070 
0071     d->init();
0072 }
0073 
0074 KRichTextEdit::KRichTextEdit(QWidget *parent)
0075     : KRichTextEdit(*new KRichTextEditPrivate(this), parent)
0076 {
0077 }
0078 
0079 KRichTextEdit::KRichTextEdit(KRichTextEditPrivate &dd, QWidget *parent)
0080     : KTextEdit(dd, parent)
0081 {
0082     Q_D(KRichTextEdit);
0083 
0084     d->init();
0085 }
0086 
0087 KRichTextEdit::~KRichTextEdit() = default;
0088 
0089 //@cond PRIVATE
0090 void KRichTextEditPrivate::init()
0091 {
0092     Q_Q(KRichTextEdit);
0093 
0094     q->setAcceptRichText(false);
0095     KCursor::setAutoHideCursor(q, true, true);
0096 }
0097 //@endcond
0098 
0099 void KRichTextEdit::setListStyle(int _styleIndex)
0100 {
0101     Q_D(KRichTextEdit);
0102 
0103     d->nestedListHelper->handleOnBulletType(-_styleIndex);
0104     setFocus();
0105     d->activateRichText();
0106 }
0107 
0108 void KRichTextEdit::indentListMore()
0109 {
0110     Q_D(KRichTextEdit);
0111 
0112     d->nestedListHelper->changeIndent(+1);
0113     d->activateRichText();
0114 }
0115 
0116 void KRichTextEdit::indentListLess()
0117 {
0118     Q_D(KRichTextEdit);
0119 
0120     d->nestedListHelper->changeIndent(-1);
0121 }
0122 
0123 void KRichTextEdit::insertHorizontalRule()
0124 {
0125     Q_D(KRichTextEdit);
0126 
0127     QTextCursor cursor = textCursor();
0128     QTextBlockFormat bf = cursor.blockFormat();
0129     QTextCharFormat cf = cursor.charFormat();
0130 
0131     cursor.beginEditBlock();
0132     cursor.insertHtml(QStringLiteral("<hr>"));
0133     cursor.insertBlock(bf, cf);
0134     cursor.endEditBlock();
0135     setTextCursor(cursor);
0136     d->activateRichText();
0137 }
0138 
0139 void KRichTextEdit::alignLeft()
0140 {
0141     Q_D(KRichTextEdit);
0142 
0143     setAlignment(Qt::AlignLeft);
0144     setFocus();
0145     d->activateRichText();
0146 }
0147 
0148 void KRichTextEdit::alignCenter()
0149 {
0150     Q_D(KRichTextEdit);
0151 
0152     setAlignment(Qt::AlignHCenter);
0153     setFocus();
0154     d->activateRichText();
0155 }
0156 
0157 void KRichTextEdit::alignRight()
0158 {
0159     Q_D(KRichTextEdit);
0160 
0161     setAlignment(Qt::AlignRight);
0162     setFocus();
0163     d->activateRichText();
0164 }
0165 
0166 void KRichTextEdit::alignJustify()
0167 {
0168     Q_D(KRichTextEdit);
0169 
0170     setAlignment(Qt::AlignJustify);
0171     setFocus();
0172     d->activateRichText();
0173 }
0174 
0175 void KRichTextEdit::makeRightToLeft()
0176 {
0177     Q_D(KRichTextEdit);
0178 
0179     QTextBlockFormat format;
0180     format.setLayoutDirection(Qt::RightToLeft);
0181     QTextCursor cursor = textCursor();
0182     cursor.mergeBlockFormat(format);
0183     setTextCursor(cursor);
0184     setFocus();
0185     d->activateRichText();
0186 }
0187 
0188 void KRichTextEdit::makeLeftToRight()
0189 {
0190     Q_D(KRichTextEdit);
0191 
0192     QTextBlockFormat format;
0193     format.setLayoutDirection(Qt::LeftToRight);
0194     QTextCursor cursor = textCursor();
0195     cursor.mergeBlockFormat(format);
0196     setTextCursor(cursor);
0197     setFocus();
0198     d->activateRichText();
0199 }
0200 
0201 void KRichTextEdit::setTextBold(bool bold)
0202 {
0203     Q_D(KRichTextEdit);
0204 
0205     QTextCharFormat fmt;
0206     fmt.setFontWeight(bold ? QFont::Bold : QFont::Normal);
0207     d->mergeFormatOnWordOrSelection(fmt);
0208     setFocus();
0209     d->activateRichText();
0210 }
0211 
0212 void KRichTextEdit::setTextItalic(bool italic)
0213 {
0214     Q_D(KRichTextEdit);
0215 
0216     QTextCharFormat fmt;
0217     fmt.setFontItalic(italic);
0218     d->mergeFormatOnWordOrSelection(fmt);
0219     setFocus();
0220     d->activateRichText();
0221 }
0222 
0223 void KRichTextEdit::setTextUnderline(bool underline)
0224 {
0225     Q_D(KRichTextEdit);
0226 
0227     QTextCharFormat fmt;
0228     fmt.setFontUnderline(underline);
0229     d->mergeFormatOnWordOrSelection(fmt);
0230     setFocus();
0231     d->activateRichText();
0232 }
0233 
0234 void KRichTextEdit::setTextStrikeOut(bool strikeOut)
0235 {
0236     Q_D(KRichTextEdit);
0237 
0238     QTextCharFormat fmt;
0239     fmt.setFontStrikeOut(strikeOut);
0240     d->mergeFormatOnWordOrSelection(fmt);
0241     setFocus();
0242     d->activateRichText();
0243 }
0244 
0245 void KRichTextEdit::setTextForegroundColor(const QColor &color)
0246 {
0247     Q_D(KRichTextEdit);
0248 
0249     QTextCharFormat fmt;
0250     fmt.setForeground(color);
0251     d->mergeFormatOnWordOrSelection(fmt);
0252     setFocus();
0253     d->activateRichText();
0254 }
0255 
0256 void KRichTextEdit::setTextBackgroundColor(const QColor &color)
0257 {
0258     Q_D(KRichTextEdit);
0259 
0260     QTextCharFormat fmt;
0261     fmt.setBackground(color);
0262     d->mergeFormatOnWordOrSelection(fmt);
0263     setFocus();
0264     d->activateRichText();
0265 }
0266 
0267 void KRichTextEdit::setFontFamily(const QString &fontFamily)
0268 {
0269     Q_D(KRichTextEdit);
0270 
0271     QTextCharFormat fmt;
0272     fmt.setFontFamilies({fontFamily});
0273     d->mergeFormatOnWordOrSelection(fmt);
0274     setFocus();
0275     d->activateRichText();
0276 }
0277 
0278 void KRichTextEdit::setFontSize(int size)
0279 {
0280     Q_D(KRichTextEdit);
0281 
0282     QTextCharFormat fmt;
0283     fmt.setFontPointSize(size);
0284     d->mergeFormatOnWordOrSelection(fmt);
0285     setFocus();
0286     d->activateRichText();
0287 }
0288 
0289 void KRichTextEdit::setFont(const QFont &font)
0290 {
0291     Q_D(KRichTextEdit);
0292 
0293     QTextCharFormat fmt;
0294     fmt.setFont(font);
0295     d->mergeFormatOnWordOrSelection(fmt);
0296     setFocus();
0297     d->activateRichText();
0298 }
0299 
0300 void KRichTextEdit::switchToPlainText()
0301 {
0302     Q_D(KRichTextEdit);
0303 
0304     if (d->mMode == Rich) {
0305         d->mMode = Plain;
0306         // TODO: Warn the user about this?
0307         auto insertPlainFunc = [this]() {
0308             insertPlainTextImplementation();
0309         };
0310         QMetaObject::invokeMethod(this, insertPlainFunc);
0311         setAcceptRichText(false);
0312         Q_EMIT textModeChanged(d->mMode);
0313     }
0314 }
0315 
0316 void KRichTextEdit::insertPlainTextImplementation()
0317 {
0318     document()->setPlainText(document()->toPlainText());
0319 }
0320 
0321 void KRichTextEdit::setTextSuperScript(bool superscript)
0322 {
0323     Q_D(KRichTextEdit);
0324 
0325     QTextCharFormat fmt;
0326     fmt.setVerticalAlignment(superscript ? QTextCharFormat::AlignSuperScript : QTextCharFormat::AlignNormal);
0327     d->mergeFormatOnWordOrSelection(fmt);
0328     setFocus();
0329     d->activateRichText();
0330 }
0331 
0332 void KRichTextEdit::setTextSubScript(bool subscript)
0333 {
0334     Q_D(KRichTextEdit);
0335 
0336     QTextCharFormat fmt;
0337     fmt.setVerticalAlignment(subscript ? QTextCharFormat::AlignSubScript : QTextCharFormat::AlignNormal);
0338     d->mergeFormatOnWordOrSelection(fmt);
0339     setFocus();
0340     d->activateRichText();
0341 }
0342 
0343 void KRichTextEdit::setHeadingLevel(int level)
0344 {
0345     Q_D(KRichTextEdit);
0346 
0347     const int boundedLevel = qBound(0, 6, level);
0348     // Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
0349     // level=2 look the same
0350     const int sizeAdjustment = boundedLevel > 0 ? 5 - boundedLevel : 0;
0351 
0352     QTextCursor cursor = textCursor();
0353     cursor.beginEditBlock();
0354 
0355     QTextBlockFormat blkfmt;
0356     blkfmt.setHeadingLevel(boundedLevel);
0357     cursor.mergeBlockFormat(blkfmt);
0358 
0359     QTextCharFormat chrfmt;
0360     chrfmt.setFontWeight(boundedLevel > 0 ? QFont::Bold : QFont::Normal);
0361     chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
0362     // Applying style to the current line or selection
0363     QTextCursor selectCursor = cursor;
0364     if (selectCursor.hasSelection()) {
0365         QTextCursor top = selectCursor;
0366         top.setPosition(qMin(top.anchor(), top.position()));
0367         top.movePosition(QTextCursor::StartOfBlock);
0368 
0369         QTextCursor bottom = selectCursor;
0370         bottom.setPosition(qMax(bottom.anchor(), bottom.position()));
0371         bottom.movePosition(QTextCursor::EndOfBlock);
0372 
0373         selectCursor.setPosition(top.position(), QTextCursor::MoveAnchor);
0374         selectCursor.setPosition(bottom.position(), QTextCursor::KeepAnchor);
0375     } else {
0376         selectCursor.select(QTextCursor::BlockUnderCursor);
0377     }
0378     selectCursor.mergeCharFormat(chrfmt);
0379 
0380     cursor.mergeBlockCharFormat(chrfmt);
0381     cursor.endEditBlock();
0382     setTextCursor(cursor);
0383     setFocus();
0384     d->activateRichText();
0385 }
0386 
0387 void KRichTextEdit::enableRichTextMode()
0388 {
0389     Q_D(KRichTextEdit);
0390 
0391     d->activateRichText();
0392 }
0393 
0394 KRichTextEdit::Mode KRichTextEdit::textMode() const
0395 {
0396     Q_D(const KRichTextEdit);
0397 
0398     return d->mMode;
0399 }
0400 
0401 QString KRichTextEdit::textOrHtml() const
0402 {
0403     if (textMode() == Rich) {
0404         return toCleanHtml();
0405     } else {
0406         return toPlainText();
0407     }
0408 }
0409 
0410 void KRichTextEdit::setTextOrHtml(const QString &text)
0411 {
0412     Q_D(KRichTextEdit);
0413 
0414     // might be rich text
0415     if (Qt::mightBeRichText(text)) {
0416         if (d->mMode == KRichTextEdit::Plain) {
0417             d->activateRichText();
0418         }
0419         setHtml(text);
0420     } else {
0421         setPlainText(text);
0422     }
0423 }
0424 
0425 // KF6 TODO: remove constness
0426 QString KRichTextEdit::currentLinkText() const
0427 {
0428     QTextCursor cursor = textCursor();
0429     selectLinkText(&cursor);
0430     return cursor.selectedText();
0431 }
0432 
0433 // KF6 TODO: remove constness
0434 void KRichTextEdit::selectLinkText() const
0435 {
0436     Q_D(const KRichTextEdit);
0437 
0438     QTextCursor cursor = textCursor();
0439     selectLinkText(&cursor);
0440     // KF6 TODO: remove const_cast
0441     const_cast<KRichTextEditPrivate *>(d)->setTextCursor(cursor);
0442 }
0443 
0444 void KRichTextEdit::selectLinkText(QTextCursor *cursor) const
0445 {
0446     // If the cursor is on a link, select the text of the link.
0447     if (cursor->charFormat().isAnchor()) {
0448         QString aHref = cursor->charFormat().anchorHref();
0449 
0450         // Move cursor to start of link
0451         while (cursor->charFormat().anchorHref() == aHref) {
0452             if (cursor->atStart()) {
0453                 break;
0454             }
0455             cursor->setPosition(cursor->position() - 1);
0456         }
0457         if (cursor->charFormat().anchorHref() != aHref) {
0458             cursor->setPosition(cursor->position() + 1, QTextCursor::KeepAnchor);
0459         }
0460 
0461         // Move selection to the end of the link
0462         while (cursor->charFormat().anchorHref() == aHref) {
0463             if (cursor->atEnd()) {
0464                 break;
0465             }
0466             cursor->setPosition(cursor->position() + 1, QTextCursor::KeepAnchor);
0467         }
0468         if (cursor->charFormat().anchorHref() != aHref) {
0469             cursor->setPosition(cursor->position() - 1, QTextCursor::KeepAnchor);
0470         }
0471     } else if (cursor->hasSelection()) {
0472         // Nothing to to. Using the currently selected text as the link text.
0473     } else {
0474         // Select current word
0475         cursor->movePosition(QTextCursor::StartOfWord);
0476         cursor->movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
0477     }
0478 }
0479 
0480 QString KRichTextEdit::currentLinkUrl() const
0481 {
0482     return textCursor().charFormat().anchorHref();
0483 }
0484 
0485 void KRichTextEdit::updateLink(const QString &linkUrl, const QString &linkText)
0486 {
0487     Q_D(KRichTextEdit);
0488 
0489     selectLinkText();
0490 
0491     QTextCursor cursor = textCursor();
0492     cursor.beginEditBlock();
0493 
0494     if (!cursor.hasSelection()) {
0495         cursor.select(QTextCursor::WordUnderCursor);
0496     }
0497 
0498     QTextCharFormat format = cursor.charFormat();
0499     // Save original format to create an extra space with the existing char
0500     // format for the block
0501     const QTextCharFormat originalFormat = format;
0502     if (!linkUrl.isEmpty()) {
0503         // Add link details
0504         format.setAnchor(true);
0505         format.setAnchorHref(linkUrl);
0506         // Workaround for QTBUG-1814:
0507         // Link formatting does not get applied immediately when setAnchor(true)
0508         // is called.  So the formatting needs to be applied manually.
0509         format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
0510         format.setUnderlineColor(KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color());
0511         format.setForeground(KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color());
0512         d->activateRichText();
0513     } else {
0514         // Remove link details
0515         format.setAnchor(false);
0516         format.setAnchorHref(QString());
0517         // Workaround for QTBUG-1814:
0518         // Link formatting does not get removed immediately when setAnchor(false)
0519         // is called. So the formatting needs to be applied manually.
0520         QTextDocument defaultTextDocument;
0521         QTextCharFormat defaultCharFormat = defaultTextDocument.begin().charFormat();
0522 
0523         format.setUnderlineStyle(defaultCharFormat.underlineStyle());
0524         format.setUnderlineColor(defaultCharFormat.underlineColor());
0525         format.setForeground(defaultCharFormat.foreground());
0526     }
0527 
0528     // Insert link text specified in dialog, otherwise write out url.
0529     QString _linkText;
0530     if (!linkText.isEmpty()) {
0531         _linkText = linkText;
0532     } else {
0533         _linkText = linkUrl;
0534     }
0535     cursor.insertText(_linkText, format);
0536 
0537     // Insert a space after the link if at the end of the block so that
0538     // typing some text after the link does not carry link formatting
0539     if (!linkUrl.isEmpty() && cursor.atBlockEnd()) {
0540         cursor.setPosition(cursor.selectionEnd());
0541         cursor.setCharFormat(originalFormat);
0542         cursor.insertText(QStringLiteral(" "));
0543     }
0544 
0545     cursor.endEditBlock();
0546 }
0547 
0548 void KRichTextEdit::keyPressEvent(QKeyEvent *event)
0549 {
0550     Q_D(KRichTextEdit);
0551 
0552     bool handled = false;
0553     if (textCursor().currentList()) {
0554         handled = d->nestedListHelper->handleKeyPressEvent(event);
0555     }
0556 
0557     // If a line was merged with previous (next) one, with different heading level,
0558     // the style should also be adjusted accordingly (i.e. merged)
0559     if ((event->key() == Qt::Key_Backspace && textCursor().atBlockStart()
0560          && (textCursor().blockFormat().headingLevel() != textCursor().block().previous().blockFormat().headingLevel()))
0561         || (event->key() == Qt::Key_Delete && textCursor().atBlockEnd()
0562             && (textCursor().blockFormat().headingLevel() != textCursor().block().next().blockFormat().headingLevel()))) {
0563         QTextCursor cursor = textCursor();
0564         cursor.beginEditBlock();
0565         if (event->key() == Qt::Key_Delete) {
0566             cursor.deleteChar();
0567         } else {
0568             cursor.deletePreviousChar();
0569         }
0570         setHeadingLevel(cursor.blockFormat().headingLevel());
0571         cursor.endEditBlock();
0572         handled = true;
0573     }
0574 
0575     const auto prevHeadingLevel = textCursor().blockFormat().headingLevel();
0576     if (!handled) {
0577         KTextEdit::keyPressEvent(event);
0578     }
0579 
0580     // Match the behavior of office suites: newline after header switches to normal text
0581     if (event->key() == Qt::Key_Return //
0582         && prevHeadingLevel > 0) {
0583         // it should be undoable together with actual "return" keypress
0584         textCursor().joinPreviousEditBlock();
0585         if (textCursor().atBlockEnd()) {
0586             setHeadingLevel(0);
0587         } else {
0588             setHeadingLevel(prevHeadingLevel);
0589         }
0590         textCursor().endEditBlock();
0591     }
0592 
0593     Q_EMIT cursorPositionChanged();
0594 }
0595 
0596 // void KRichTextEdit::dropEvent(QDropEvent *event)
0597 // {
0598 //     int dropSize = event->mimeData()->text().size();
0599 //
0600 //     dropEvent( event );
0601 //     QTextCursor cursor = textCursor();
0602 //     int cursorPosition = cursor.position();
0603 //     cursor.setPosition( cursorPosition - dropSize );
0604 //     cursor.setPosition( cursorPosition, QTextCursor::KeepAnchor );
0605 //     setTextCursor( cursor );
0606 //     d->nestedListHelper->handleAfterDropEvent( event );
0607 // }
0608 
0609 bool KRichTextEdit::canIndentList() const
0610 {
0611     Q_D(const KRichTextEdit);
0612 
0613     return d->nestedListHelper->canIndent();
0614 }
0615 
0616 bool KRichTextEdit::canDedentList() const
0617 {
0618     Q_D(const KRichTextEdit);
0619 
0620     return d->nestedListHelper->canDedent();
0621 }
0622 
0623 QString KRichTextEdit::toCleanHtml() const
0624 {
0625     QString result = toHtml();
0626 
0627     static const QString EMPTYLINEHTML = QLatin1String(
0628         "<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; "
0629         "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; \">&nbsp;</p>");
0630 
0631     // Qt inserts various style properties based on the current mode of the editor (underline,
0632     // bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'.
0633     static const QString EMPTYLINEREGEX = QStringLiteral("<p style=\"-qt-paragraph-type:empty;(.*?)</p>");
0634 
0635     static const QString OLLISTPATTERNQT = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
0636 
0637     static const QString ULLISTPATTERNQT = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
0638 
0639     static const QString ORDEREDLISTHTML = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px;");
0640 
0641     static const QString UNORDEREDLISTHTML = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px;");
0642 
0643     // fix 1 - empty lines should show as empty lines - MS Outlook treats margin-top:0px; as
0644     // a non-existing line.
0645     // Although we can simply remove the margin-top style property, we still get unwanted results
0646     // if you have three or more empty lines. It's best to replace empty <p> elements with <p>&nbsp;</p>.
0647     // replace all occurrences with the new line text
0648     result.replace(QRegularExpression(EMPTYLINEREGEX), EMPTYLINEHTML);
0649 
0650     // fix 2a - ordered lists - MS Outlook treats margin-left:0px; as
0651     // a non-existing number; e.g: "1. First item" turns into "First Item"
0652     result.replace(OLLISTPATTERNQT, ORDEREDLISTHTML);
0653 
0654     // fix 2b - unordered lists - MS Outlook treats margin-left:0px; as
0655     // a non-existing bullet; e.g: "* First bullet" turns into "First Bullet"
0656     result.replace(ULLISTPATTERNQT, UNORDEREDLISTHTML);
0657 
0658     return result;
0659 }
0660 
0661 #include "moc_krichtextedit.cpp"