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; \"> </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> </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"