File indexing completed on 2024-12-22 04:28:12
0001 /* 0002 SPDX-FileCopyrightText: 2023-2024 Laurent Montel <montel.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "richtextbrowser.h" 0008 0009 #include "widgets/textmessageindicator.h" 0010 #include <KCursor> 0011 #include <KLocalizedString> 0012 #include <KMessageBox> 0013 #include <KStandardAction> 0014 #include <KStandardGuiItem> 0015 #include <QIcon> 0016 0017 #include "config-textcustomeditor.h" 0018 #include <KIO/KUriFilterSearchProviderActions> 0019 #if HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT 0020 #include <TextEditTextToSpeech/TextToSpeech> 0021 #endif 0022 0023 #include <KColorScheme> 0024 #include <QApplication> 0025 #include <QClipboard> 0026 #include <QContextMenuEvent> 0027 #include <QMenu> 0028 #include <QScrollBar> 0029 #include <QTextBlock> 0030 #include <QTextCursor> 0031 #include <QTextDocumentFragment> 0032 0033 using namespace TextCustomEditor; 0034 class Q_DECL_HIDDEN RichTextBrowser::RichTextBrowserPrivate 0035 { 0036 public: 0037 RichTextBrowserPrivate(RichTextBrowser *qq) 0038 : q(qq) 0039 , textIndicator(new TextCustomEditor::TextMessageIndicator(q)) 0040 , webshortcutMenuManager(new KIO::KUriFilterSearchProviderActions(q)) 0041 { 0042 supportFeatures |= RichTextBrowser::Search; 0043 supportFeatures |= RichTextBrowser::TextToSpeech; 0044 supportFeatures |= RichTextBrowser::AllowWebShortcut; 0045 0046 // Workaround QTextEdit behavior: if the cursor points right after the link 0047 // and start typing, the char format is kept. If user wants to write normal 0048 // text right after the link, the only way is to move cursor at the next character 0049 // (say for "<a>text</a>more text" the character has to be before letter "o"!) 0050 // It's impossible if the whole document ends with a link. 0051 // The same happens when text starts with a link: it's impossible to write normal text before it. 0052 QObject::connect(q, &RichTextBrowser::cursorPositionChanged, q, [this]() { 0053 QTextCursor c = q->textCursor(); 0054 if (c.charFormat().isAnchor() && !c.hasSelection()) { 0055 QTextCharFormat fmt; 0056 // If we are at block start or end (and at anchor), we just set the "default" format 0057 if (!c.atBlockEnd() && !c.atBlockStart() && !c.hasSelection()) { 0058 QTextCursor probe = c; 0059 // Otherwise, if the next character is not a link, we just grab it's format 0060 probe.movePosition(QTextCursor::NextCharacter); 0061 if (!probe.charFormat().isAnchor()) { 0062 fmt = probe.charFormat(); 0063 } 0064 } 0065 c.setCharFormat(fmt); 0066 q->setTextCursor(c); 0067 } 0068 }); 0069 } 0070 0071 ~RichTextBrowserPrivate() 0072 { 0073 } 0074 0075 RichTextBrowser *const q; 0076 TextCustomEditor::TextMessageIndicator *const textIndicator; 0077 QTextDocumentFragment originalDoc; 0078 KIO::KUriFilterSearchProviderActions *const webshortcutMenuManager; 0079 RichTextBrowser::SupportFeatures supportFeatures; 0080 QColor mReadOnlyBackgroundColor; 0081 int mInitialFontSize; 0082 bool customPalette = false; 0083 }; 0084 0085 RichTextBrowser::RichTextBrowser(QWidget *parent) 0086 : QTextBrowser(parent) 0087 , d(new RichTextBrowserPrivate(this)) 0088 { 0089 setAcceptRichText(true); 0090 KCursor::setAutoHideCursor(this, true, false); 0091 d->mInitialFontSize = font().pointSize(); 0092 regenerateColorScheme(); 0093 } 0094 0095 RichTextBrowser::~RichTextBrowser() = default; 0096 0097 void RichTextBrowser::regenerateColorScheme() 0098 { 0099 d->mReadOnlyBackgroundColor = KColorScheme(QPalette::Disabled, KColorScheme::View).background().color(); 0100 updateReadOnlyColor(); 0101 } 0102 0103 void RichTextBrowser::setDefaultFontSize(int val) 0104 { 0105 d->mInitialFontSize = val; 0106 slotZoomReset(); 0107 } 0108 0109 void RichTextBrowser::slotDisplayMessageIndicator(const QString &message) 0110 { 0111 d->textIndicator->display(message); 0112 } 0113 0114 void RichTextBrowser::contextMenuEvent(QContextMenuEvent *event) 0115 { 0116 QMenu *popup = mousePopupMenu(event->pos()); 0117 if (popup) { 0118 popup->exec(event->globalPos()); 0119 delete popup; 0120 } 0121 } 0122 0123 QMenu *RichTextBrowser::mousePopupMenu(QPoint pos) 0124 { 0125 QMenu *popup = createStandardContextMenu(); 0126 if (popup) { 0127 const bool emptyDocument = document()->isEmpty(); 0128 if (!isReadOnly()) { 0129 const QList<QAction *> actionList = popup->actions(); 0130 enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs }; 0131 QAction *separatorAction = nullptr; 0132 const int idx = actionList.indexOf(actionList[SelectAllAct]) + 1; 0133 if (idx < actionList.count()) { 0134 separatorAction = actionList.at(idx); 0135 } 0136 if (separatorAction) { 0137 QAction *clearAllAction = KStandardAction::clear(this, &RichTextBrowser::slotUndoableClear, popup); 0138 if (emptyDocument) { 0139 clearAllAction->setEnabled(false); 0140 } 0141 popup->insertAction(separatorAction, clearAllAction); 0142 } 0143 } 0144 if (searchSupport()) { 0145 popup->addSeparator(); 0146 QAction *findAction = KStandardAction::find(this, &RichTextBrowser::findText, popup); 0147 popup->addAction(findAction); 0148 if (emptyDocument) { 0149 findAction->setEnabled(false); 0150 } 0151 } else { 0152 popup->addSeparator(); 0153 } 0154 0155 #if HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT 0156 if (!emptyDocument) { 0157 QAction *speakAction = popup->addAction(i18n("Speak Text")); 0158 speakAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech"))); 0159 connect(speakAction, &QAction::triggered, this, &RichTextBrowser::slotSpeakText); 0160 } 0161 #endif 0162 if (webShortcutSupport() && textCursor().hasSelection()) { 0163 popup->addSeparator(); 0164 const QString selectedText = textCursor().selectedText(); 0165 d->webshortcutMenuManager->setSelectedText(selectedText); 0166 d->webshortcutMenuManager->addWebShortcutsToMenu(popup); 0167 } 0168 addExtraMenuEntry(popup, pos); 0169 return popup; 0170 } 0171 return nullptr; 0172 } 0173 0174 void RichTextBrowser::slotSpeakText() 0175 { 0176 QString text; 0177 if (textCursor().hasSelection()) { 0178 text = textCursor().selectedText(); 0179 } else { 0180 text = toPlainText(); 0181 } 0182 Q_EMIT say(text); 0183 } 0184 0185 void RichTextBrowser::setWebShortcutSupport(bool b) 0186 { 0187 if (b) { 0188 d->supportFeatures |= AllowWebShortcut; 0189 } else { 0190 d->supportFeatures = (d->supportFeatures & ~AllowWebShortcut); 0191 } 0192 } 0193 0194 bool RichTextBrowser::webShortcutSupport() const 0195 { 0196 return d->supportFeatures & AllowWebShortcut; 0197 } 0198 0199 void RichTextBrowser::setSearchSupport(bool b) 0200 { 0201 if (b) { 0202 d->supportFeatures |= Search; 0203 } else { 0204 d->supportFeatures = (d->supportFeatures & ~Search); 0205 } 0206 } 0207 0208 bool RichTextBrowser::searchSupport() const 0209 { 0210 return d->supportFeatures & Search; 0211 } 0212 0213 void RichTextBrowser::setTextToSpeechSupport(bool b) 0214 { 0215 if (b) { 0216 d->supportFeatures |= TextToSpeech; 0217 } else { 0218 d->supportFeatures = (d->supportFeatures & ~TextToSpeech); 0219 } 0220 } 0221 0222 bool RichTextBrowser::textToSpeechSupport() const 0223 { 0224 return d->supportFeatures & TextToSpeech; 0225 } 0226 0227 void RichTextBrowser::addExtraMenuEntry(QMenu *menu, QPoint pos) 0228 { 0229 Q_UNUSED(menu) 0230 Q_UNUSED(pos) 0231 } 0232 0233 void RichTextBrowser::slotUndoableClear() 0234 { 0235 QTextCursor cursor = textCursor(); 0236 cursor.beginEditBlock(); 0237 cursor.movePosition(QTextCursor::Start); 0238 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); 0239 cursor.removeSelectedText(); 0240 cursor.endEditBlock(); 0241 } 0242 0243 void RichTextBrowser::updateReadOnlyColor() 0244 { 0245 if (isReadOnly()) { 0246 QPalette p = palette(); 0247 p.setColor(QPalette::Base, d->mReadOnlyBackgroundColor); 0248 p.setColor(QPalette::Window, d->mReadOnlyBackgroundColor); 0249 setPalette(p); 0250 } 0251 } 0252 0253 static void richTextDeleteWord(QTextCursor cursor, QTextCursor::MoveOperation op) 0254 { 0255 cursor.clearSelection(); 0256 cursor.movePosition(op, QTextCursor::KeepAnchor); 0257 cursor.removeSelectedText(); 0258 } 0259 0260 void RichTextBrowser::deleteWordBack() 0261 { 0262 richTextDeleteWord(textCursor(), QTextCursor::PreviousWord); 0263 } 0264 0265 void RichTextBrowser::deleteWordForward() 0266 { 0267 richTextDeleteWord(textCursor(), QTextCursor::WordRight); 0268 } 0269 0270 bool RichTextBrowser::event(QEvent *ev) 0271 { 0272 if (ev->type() == QEvent::ShortcutOverride) { 0273 auto e = static_cast<QKeyEvent *>(ev); 0274 if (overrideShortcut(e)) { 0275 e->accept(); 0276 return true; 0277 } 0278 } else if (ev->type() == QEvent::ApplicationPaletteChange) { 0279 regenerateColorScheme(); 0280 } 0281 return QTextEdit::event(ev); 0282 } 0283 0284 void RichTextBrowser::wheelEvent(QWheelEvent *event) 0285 { 0286 if (QApplication::keyboardModifiers() & Qt::ControlModifier) { 0287 const int angleDeltaY{event->angleDelta().y()}; 0288 if (angleDeltaY > 0) { 0289 zoomIn(); 0290 } else if (angleDeltaY < 0) { 0291 zoomOut(); 0292 } 0293 event->accept(); 0294 return; 0295 } 0296 QTextEdit::wheelEvent(event); 0297 } 0298 0299 bool RichTextBrowser::handleShortcut(QKeyEvent *event) 0300 { 0301 const int key = event->key() | event->modifiers(); 0302 0303 if (KStandardShortcut::copy().contains(key)) { 0304 copy(); 0305 return true; 0306 } else if (KStandardShortcut::paste().contains(key)) { 0307 paste(); 0308 return true; 0309 } else if (KStandardShortcut::cut().contains(key)) { 0310 cut(); 0311 return true; 0312 } else if (KStandardShortcut::undo().contains(key)) { 0313 if (!isReadOnly()) { 0314 undo(); 0315 } 0316 return true; 0317 } else if (KStandardShortcut::redo().contains(key)) { 0318 if (!isReadOnly()) { 0319 redo(); 0320 } 0321 return true; 0322 } else if (KStandardShortcut::deleteWordBack().contains(key)) { 0323 if (!isReadOnly()) { 0324 deleteWordBack(); 0325 } 0326 return true; 0327 } else if (KStandardShortcut::deleteWordForward().contains(key)) { 0328 if (!isReadOnly()) { 0329 deleteWordForward(); 0330 } 0331 return true; 0332 } else if (KStandardShortcut::backwardWord().contains(key)) { 0333 QTextCursor cursor = textCursor(); 0334 cursor.movePosition(QTextCursor::PreviousWord); 0335 setTextCursor(cursor); 0336 return true; 0337 } else if (KStandardShortcut::forwardWord().contains(key)) { 0338 QTextCursor cursor = textCursor(); 0339 cursor.movePosition(QTextCursor::NextWord); 0340 setTextCursor(cursor); 0341 return true; 0342 } else if (KStandardShortcut::next().contains(key)) { 0343 QTextCursor cursor = textCursor(); 0344 bool moved = false; 0345 qreal lastY = cursorRect(cursor).bottom(); 0346 qreal distance = 0; 0347 do { 0348 qreal y = cursorRect(cursor).bottom(); 0349 distance += qAbs(y - lastY); 0350 lastY = y; 0351 moved = cursor.movePosition(QTextCursor::Down); 0352 } while (moved && distance < viewport()->height()); 0353 0354 if (moved) { 0355 cursor.movePosition(QTextCursor::Up); 0356 verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd); 0357 } 0358 setTextCursor(cursor); 0359 return true; 0360 } else if (KStandardShortcut::prior().contains(key)) { 0361 QTextCursor cursor = textCursor(); 0362 bool moved = false; 0363 qreal lastY = cursorRect(cursor).bottom(); 0364 qreal distance = 0; 0365 do { 0366 qreal y = cursorRect(cursor).bottom(); 0367 distance += qAbs(y - lastY); 0368 lastY = y; 0369 moved = cursor.movePosition(QTextCursor::Up); 0370 } while (moved && distance < viewport()->height()); 0371 0372 if (moved) { 0373 cursor.movePosition(QTextCursor::Down); 0374 verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub); 0375 } 0376 setTextCursor(cursor); 0377 return true; 0378 } else if (KStandardShortcut::begin().contains(key)) { 0379 QTextCursor cursor = textCursor(); 0380 cursor.movePosition(QTextCursor::Start); 0381 setTextCursor(cursor); 0382 return true; 0383 } else if (KStandardShortcut::end().contains(key)) { 0384 QTextCursor cursor = textCursor(); 0385 cursor.movePosition(QTextCursor::End); 0386 setTextCursor(cursor); 0387 return true; 0388 } else if (KStandardShortcut::beginningOfLine().contains(key)) { 0389 QTextCursor cursor = textCursor(); 0390 cursor.movePosition(QTextCursor::StartOfLine); 0391 setTextCursor(cursor); 0392 return true; 0393 } else if (KStandardShortcut::endOfLine().contains(key)) { 0394 QTextCursor cursor = textCursor(); 0395 cursor.movePosition(QTextCursor::EndOfLine); 0396 setTextCursor(cursor); 0397 return true; 0398 } else if (searchSupport() && KStandardShortcut::find().contains(key)) { 0399 Q_EMIT findText(); 0400 return true; 0401 } else if (KStandardShortcut::pasteSelection().contains(key)) { 0402 QString text = QApplication::clipboard()->text(QClipboard::Selection); 0403 if (!text.isEmpty()) { 0404 insertPlainText(text); // TODO: check if this is html? (MiB) 0405 } 0406 return true; 0407 } else if (event == QKeySequence::DeleteEndOfLine) { 0408 QTextCursor cursor = textCursor(); 0409 QTextBlock block = cursor.block(); 0410 if (cursor.position() == block.position() + block.length() - 2) { 0411 cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); 0412 } else { 0413 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 0414 } 0415 cursor.removeSelectedText(); 0416 setTextCursor(cursor); 0417 return true; 0418 } 0419 0420 return false; 0421 } 0422 0423 bool RichTextBrowser::overrideShortcut(QKeyEvent *event) 0424 { 0425 const int key = event->key() | event->modifiers(); 0426 0427 if (KStandardShortcut::copy().contains(key)) { 0428 return true; 0429 } else if (KStandardShortcut::paste().contains(key)) { 0430 return true; 0431 } else if (KStandardShortcut::cut().contains(key)) { 0432 return true; 0433 } else if (KStandardShortcut::undo().contains(key)) { 0434 return true; 0435 } else if (KStandardShortcut::redo().contains(key)) { 0436 return true; 0437 } else if (KStandardShortcut::deleteWordBack().contains(key)) { 0438 return true; 0439 } else if (KStandardShortcut::deleteWordForward().contains(key)) { 0440 return true; 0441 } else if (KStandardShortcut::backwardWord().contains(key)) { 0442 return true; 0443 } else if (KStandardShortcut::forwardWord().contains(key)) { 0444 return true; 0445 } else if (KStandardShortcut::next().contains(key)) { 0446 return true; 0447 } else if (KStandardShortcut::prior().contains(key)) { 0448 return true; 0449 } else if (KStandardShortcut::begin().contains(key)) { 0450 return true; 0451 } else if (KStandardShortcut::end().contains(key)) { 0452 return true; 0453 } else if (KStandardShortcut::beginningOfLine().contains(key)) { 0454 return true; 0455 } else if (KStandardShortcut::endOfLine().contains(key)) { 0456 return true; 0457 } else if (KStandardShortcut::pasteSelection().contains(key)) { 0458 return true; 0459 } else if (searchSupport() && KStandardShortcut::find().contains(key)) { 0460 return true; 0461 } else if (searchSupport() && KStandardShortcut::findNext().contains(key)) { 0462 return true; 0463 } else if (event->matches(QKeySequence::SelectAll)) { // currently missing in QTextEdit 0464 return true; 0465 } else if (event == QKeySequence::DeleteEndOfLine) { 0466 return true; 0467 } 0468 return false; 0469 } 0470 0471 void RichTextBrowser::keyPressEvent(QKeyEvent *event) 0472 { 0473 const bool isControlClicked = event->modifiers() & Qt::ControlModifier; 0474 const bool isShiftClicked = event->modifiers() & Qt::ShiftModifier; 0475 if (handleShortcut(event)) { 0476 event->accept(); 0477 } else if (event->key() == Qt::Key_Up && isControlClicked && isShiftClicked) { 0478 moveLineUpDown(true); 0479 event->accept(); 0480 } else if (event->key() == Qt::Key_Down && isControlClicked && isShiftClicked) { 0481 moveLineUpDown(false); 0482 event->accept(); 0483 } else if (event->key() == Qt::Key_Up && isControlClicked) { 0484 moveCursorBeginUpDown(true); 0485 event->accept(); 0486 } else if (event->key() == Qt::Key_Down && isControlClicked) { 0487 moveCursorBeginUpDown(false); 0488 event->accept(); 0489 } else { 0490 QTextEdit::keyPressEvent(event); 0491 } 0492 } 0493 0494 int RichTextBrowser::zoomFactor() const 0495 { 0496 int pourcentage = 100; 0497 const QFont f = font(); 0498 if (d->mInitialFontSize != f.pointSize()) { 0499 pourcentage = (f.pointSize() * 100) / d->mInitialFontSize; 0500 } 0501 return pourcentage; 0502 } 0503 0504 void RichTextBrowser::slotZoomReset() 0505 { 0506 QFont f = font(); 0507 if (d->mInitialFontSize != f.pointSize()) { 0508 f.setPointSize(d->mInitialFontSize); 0509 setFont(f); 0510 } 0511 } 0512 0513 void RichTextBrowser::moveCursorBeginUpDown(bool moveUp) 0514 { 0515 QTextCursor cursor = textCursor(); 0516 QTextCursor move = cursor; 0517 move.beginEditBlock(); 0518 cursor.clearSelection(); 0519 move.movePosition(QTextCursor::StartOfBlock); 0520 move.movePosition(moveUp ? QTextCursor::PreviousBlock : QTextCursor::NextBlock); 0521 move.endEditBlock(); 0522 setTextCursor(move); 0523 } 0524 0525 void RichTextBrowser::moveLineUpDown(bool moveUp) 0526 { 0527 QTextCursor cursor = textCursor(); 0528 QTextCursor move = cursor; 0529 move.beginEditBlock(); 0530 0531 const bool hasSelection = cursor.hasSelection(); 0532 0533 if (hasSelection) { 0534 move.setPosition(cursor.selectionStart()); 0535 move.movePosition(QTextCursor::StartOfBlock); 0536 move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor); 0537 move.movePosition(move.atBlockStart() ? QTextCursor::Left : QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 0538 } else { 0539 move.movePosition(QTextCursor::StartOfBlock); 0540 move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 0541 } 0542 const QString text = move.selectedText(); 0543 0544 move.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); 0545 move.removeSelectedText(); 0546 0547 if (moveUp) { 0548 move.movePosition(QTextCursor::PreviousBlock); 0549 move.insertBlock(); 0550 move.movePosition(QTextCursor::Left); 0551 } else { 0552 move.movePosition(QTextCursor::EndOfBlock); 0553 if (move.atBlockStart()) { // empty block 0554 move.movePosition(QTextCursor::NextBlock); 0555 move.insertBlock(); 0556 move.movePosition(QTextCursor::Left); 0557 } else { 0558 move.insertBlock(); 0559 } 0560 } 0561 0562 int start = move.position(); 0563 move.clearSelection(); 0564 move.insertText(text); 0565 int end = move.position(); 0566 0567 if (hasSelection) { 0568 move.setPosition(end); 0569 move.setPosition(start, QTextCursor::KeepAnchor); 0570 } else { 0571 move.setPosition(start); 0572 } 0573 move.endEditBlock(); 0574 0575 setTextCursor(move); 0576 } 0577 0578 #include "moc_richtextbrowser.cpp"