File indexing completed on 2024-12-22 04:28:13
0001 /* 0002 SPDX-FileCopyrightText: 2013-2024 Laurent Montel <montel@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "richtexteditor.h" 0008 #include "textcustomeditor_debug.h" 0009 0010 #include "widgets/textmessageindicator.h" 0011 #include <KConfig> 0012 #include <KConfigGroup> 0013 #include <KCursor> 0014 #include <KLocalizedString> 0015 #include <KMessageBox> 0016 #include <KSharedConfig> 0017 #include <KStandardAction> 0018 #include <KStandardGuiItem> 0019 #include <QActionGroup> 0020 #include <QIcon> 0021 0022 #include "config-textcustomeditor.h" 0023 #include <KIO/KUriFilterSearchProviderActions> 0024 #include <Sonnet/Dialog> 0025 #include <Sonnet/Highlighter> 0026 #include <sonnet/backgroundchecker.h> 0027 #include <sonnet/spellcheckdecorator.h> 0028 #include <sonnet/speller.h> 0029 #if HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT 0030 #include <TextEditTextToSpeech/TextToSpeech> 0031 #endif 0032 #include <TextEmoticonsWidgets/EmoticonTextEditAction> 0033 0034 #include <KColorScheme> 0035 #include <QApplication> 0036 #include <QClipboard> 0037 #include <QContextMenuEvent> 0038 #include <QDBusConnection> 0039 #include <QDBusConnectionInterface> 0040 #include <QDialogButtonBox> 0041 #include <QMenu> 0042 #include <QPushButton> 0043 #include <QScrollBar> 0044 #include <QTextCursor> 0045 #include <QTextDocumentFragment> 0046 0047 using namespace TextCustomEditor; 0048 class Q_DECL_HIDDEN RichTextEditor::RichTextEditorPrivate 0049 { 0050 public: 0051 RichTextEditorPrivate(RichTextEditor *qq) 0052 : q(qq) 0053 , textIndicator(new TextCustomEditor::TextMessageIndicator(q)) 0054 , webshortcutMenuManager(new KIO::KUriFilterSearchProviderActions(q)) 0055 { 0056 KConfig sonnetKConfig(QStringLiteral("sonnetrc")); 0057 KConfigGroup group(&sonnetKConfig, QLatin1String("Spelling")); 0058 checkSpellingEnabled = group.readEntry("checkerEnabledByDefault", false); 0059 supportFeatures |= RichTextEditor::Search; 0060 supportFeatures |= RichTextEditor::SpellChecking; 0061 supportFeatures |= RichTextEditor::TextToSpeech; 0062 supportFeatures |= RichTextEditor::AllowTab; 0063 supportFeatures |= RichTextEditor::AllowWebShortcut; 0064 0065 // Workaround QTextEdit behavior: if the cursor points right after the link 0066 // and start typing, the char format is kept. If user wants to write normal 0067 // text right after the link, the only way is to move cursor at the next character 0068 // (say for "<a>text</a>more text" the character has to be before letter "o"!) 0069 // It's impossible if the whole document ends with a link. 0070 // The same happens when text starts with a link: it's impossible to write normal text before it. 0071 QObject::connect(q, &RichTextEditor::cursorPositionChanged, q, [this]() { 0072 QTextCursor c = q->textCursor(); 0073 if (c.charFormat().isAnchor() && !c.hasSelection()) { 0074 QTextCharFormat fmt; 0075 // If we are at block start or end (and at anchor), we just set the "default" format 0076 if (!c.atBlockEnd() && !c.atBlockStart() && !c.hasSelection()) { 0077 QTextCursor probe = c; 0078 // Otherwise, if the next character is not a link, we just grab it's format 0079 probe.movePosition(QTextCursor::NextCharacter); 0080 if (!probe.charFormat().isAnchor()) { 0081 fmt = probe.charFormat(); 0082 } 0083 } 0084 c.setCharFormat(fmt); 0085 q->setTextCursor(c); 0086 } 0087 }); 0088 } 0089 0090 ~RichTextEditorPrivate() 0091 { 0092 delete richTextDecorator; 0093 delete speller; 0094 } 0095 0096 QStringList ignoreSpellCheckingWords; 0097 RichTextEditor *const q; 0098 TextCustomEditor::TextMessageIndicator *const textIndicator; 0099 QString spellCheckingConfigFileName; 0100 QString spellCheckingLanguage; 0101 QTextDocumentFragment originalDoc; 0102 Sonnet::SpellCheckDecorator *richTextDecorator = nullptr; 0103 Sonnet::Speller *speller = nullptr; 0104 KIO::KUriFilterSearchProviderActions *const webshortcutMenuManager; 0105 RichTextEditor::SupportFeatures supportFeatures; 0106 QColor mReadOnlyBackgroundColor; 0107 int mInitialFontSize; 0108 bool customPalette = false; 0109 bool checkSpellingEnabled = false; 0110 bool activateLanguageMenu = true; 0111 bool showAutoCorrectionButton = false; 0112 }; 0113 0114 RichTextEditor::RichTextEditor(QWidget *parent) 0115 : QTextEdit(parent) 0116 , d(new RichTextEditorPrivate(this)) 0117 { 0118 setAcceptRichText(true); 0119 KCursor::setAutoHideCursor(this, true, false); 0120 setSpellCheckingConfigFileName(QString()); 0121 d->mInitialFontSize = font().pointSize(); 0122 regenerateColorScheme(); 0123 } 0124 0125 RichTextEditor::~RichTextEditor() = default; 0126 0127 void RichTextEditor::regenerateColorScheme() 0128 { 0129 d->mReadOnlyBackgroundColor = KColorScheme(QPalette::Disabled, KColorScheme::View).background().color(); 0130 updateReadOnlyColor(); 0131 } 0132 0133 void RichTextEditor::setDefaultFontSize(int val) 0134 { 0135 d->mInitialFontSize = val; 0136 slotZoomReset(); 0137 } 0138 0139 void RichTextEditor::slotDisplayMessageIndicator(const QString &message) 0140 { 0141 d->textIndicator->display(message); 0142 } 0143 0144 Sonnet::Highlighter *RichTextEditor::highlighter() const 0145 { 0146 if (d->richTextDecorator) { 0147 return d->richTextDecorator->highlighter(); 0148 } else { 0149 return nullptr; 0150 } 0151 } 0152 0153 bool RichTextEditor::activateLanguageMenu() const 0154 { 0155 return d->activateLanguageMenu; 0156 } 0157 0158 void RichTextEditor::setActivateLanguageMenu(bool activate) 0159 { 0160 d->activateLanguageMenu = activate; 0161 } 0162 0163 void RichTextEditor::contextMenuEvent(QContextMenuEvent *event) 0164 { 0165 QMenu *popup = mousePopupMenu(event->pos()); 0166 if (popup) { 0167 popup->exec(event->globalPos()); 0168 delete popup; 0169 } 0170 } 0171 0172 QMenu *RichTextEditor::mousePopupMenu(QPoint pos) 0173 { 0174 QMenu *popup = createStandardContextMenu(); 0175 if (popup) { 0176 const bool emptyDocument = document()->isEmpty(); 0177 if (!isReadOnly()) { 0178 const QList<QAction *> actionList = popup->actions(); 0179 enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs }; 0180 QAction *separatorAction = nullptr; 0181 const int idx = actionList.indexOf(actionList[SelectAllAct]) + 1; 0182 if (idx < actionList.count()) { 0183 separatorAction = actionList.at(idx); 0184 } 0185 if (separatorAction) { 0186 QAction *clearAllAction = KStandardAction::clear(this, &RichTextEditor::slotUndoableClear, popup); 0187 if (emptyDocument) { 0188 clearAllAction->setEnabled(false); 0189 } 0190 popup->insertAction(separatorAction, clearAllAction); 0191 } 0192 } 0193 if (searchSupport()) { 0194 popup->addSeparator(); 0195 QAction *findAction = KStandardAction::find(this, &RichTextEditor::findText, popup); 0196 popup->addAction(findAction); 0197 if (emptyDocument) { 0198 findAction->setEnabled(false); 0199 } 0200 popup->addSeparator(); 0201 if (!isReadOnly()) { 0202 QAction *act = KStandardAction::replace(this, &RichTextEditor::replaceText, popup); 0203 popup->addAction(act); 0204 if (emptyDocument) { 0205 act->setEnabled(false); 0206 } 0207 popup->addSeparator(); 0208 } 0209 } else { 0210 popup->addSeparator(); 0211 } 0212 0213 if (!isReadOnly() && spellCheckingSupport()) { 0214 if (!d->speller) { 0215 d->speller = new Sonnet::Speller(); 0216 } 0217 if (!d->speller->availableBackends().isEmpty()) { 0218 QAction *spellCheckAction = popup->addAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), 0219 i18n("Check Spelling..."), 0220 this, 0221 &RichTextEditor::slotCheckSpelling); 0222 if (emptyDocument) { 0223 spellCheckAction->setEnabled(false); 0224 } 0225 popup->addSeparator(); 0226 QAction *autoSpellCheckAction = popup->addAction(i18n("Auto Spell Check"), this, &RichTextEditor::slotToggleAutoSpellCheck); 0227 autoSpellCheckAction->setCheckable(true); 0228 autoSpellCheckAction->setChecked(checkSpellingEnabled()); 0229 popup->addAction(autoSpellCheckAction); 0230 0231 if (checkSpellingEnabled() && d->activateLanguageMenu) { 0232 auto languagesMenu = new QMenu(i18n("Spell Checking Language"), popup); 0233 auto languagesGroup = new QActionGroup(languagesMenu); 0234 languagesGroup->setExclusive(true); 0235 0236 QString defaultSpellcheckingLanguage = spellCheckingLanguage(); 0237 if (defaultSpellcheckingLanguage.isEmpty()) { 0238 defaultSpellcheckingLanguage = d->speller->defaultLanguage(); 0239 } 0240 0241 QMapIterator<QString, QString> i(d->speller->availableDictionaries()); 0242 while (i.hasNext()) { 0243 i.next(); 0244 QAction *languageAction = languagesMenu->addAction(i.key()); 0245 languageAction->setCheckable(true); 0246 languageAction->setChecked(defaultSpellcheckingLanguage == i.value()); 0247 languageAction->setData(i.value()); 0248 languageAction->setActionGroup(languagesGroup); 0249 connect(languageAction, &QAction::triggered, this, &RichTextEditor::slotLanguageSelected); 0250 } 0251 popup->addMenu(languagesMenu); 0252 } 0253 popup->addSeparator(); 0254 } 0255 } 0256 0257 if (allowTabSupport() && !isReadOnly()) { 0258 QAction *allowTabAction = popup->addAction(i18n("Allow Tabulations")); 0259 allowTabAction->setCheckable(true); 0260 allowTabAction->setChecked(!tabChangesFocus()); 0261 connect(allowTabAction, &QAction::triggered, this, &RichTextEditor::slotAllowTab); 0262 } 0263 #if HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT 0264 if (!emptyDocument) { 0265 QAction *speakAction = popup->addAction(i18n("Speak Text")); 0266 speakAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech"))); 0267 connect(speakAction, &QAction::triggered, this, &RichTextEditor::slotSpeakText); 0268 } 0269 #endif 0270 if (webShortcutSupport() && textCursor().hasSelection()) { 0271 popup->addSeparator(); 0272 const QString selectedText = textCursor().selectedText(); 0273 d->webshortcutMenuManager->setSelectedText(selectedText); 0274 d->webshortcutMenuManager->addWebShortcutsToMenu(popup); 0275 } 0276 if (emojiSupport()) { 0277 popup->addSeparator(); 0278 auto action = new TextEmoticonsWidgets::EmoticonTextEditAction(this); 0279 popup->addAction(action); 0280 connect(action, &TextEmoticonsWidgets::EmoticonTextEditAction::insertEmoticon, this, &RichTextEditor::slotInsertEmoticon); 0281 } 0282 addExtraMenuEntry(popup, pos); 0283 return popup; 0284 } 0285 return nullptr; 0286 } 0287 0288 void RichTextEditor::slotInsertEmoticon(const QString &str) 0289 { 0290 insertPlainText(str); 0291 } 0292 0293 void RichTextEditor::slotSpeakText() 0294 { 0295 QString text; 0296 if (textCursor().hasSelection()) { 0297 text = textCursor().selectedText(); 0298 } else { 0299 text = toPlainText(); 0300 } 0301 Q_EMIT say(text); 0302 } 0303 0304 void RichTextEditor::setWebShortcutSupport(bool b) 0305 { 0306 if (b) { 0307 d->supportFeatures |= AllowWebShortcut; 0308 } else { 0309 d->supportFeatures = (d->supportFeatures & ~AllowWebShortcut); 0310 } 0311 } 0312 0313 bool RichTextEditor::webShortcutSupport() const 0314 { 0315 return d->supportFeatures & AllowWebShortcut; 0316 } 0317 0318 void RichTextEditor::setEmojiSupport(bool b) 0319 { 0320 if (b) { 0321 d->supportFeatures |= Emoji; 0322 } else { 0323 d->supportFeatures = (d->supportFeatures & ~Emoji); 0324 } 0325 } 0326 0327 bool RichTextEditor::emojiSupport() const 0328 { 0329 return d->supportFeatures & Emoji; 0330 } 0331 0332 void RichTextEditor::addIgnoreWords(const QStringList &lst) 0333 { 0334 d->ignoreSpellCheckingWords = lst; 0335 addIgnoreWordsToHighLighter(); 0336 } 0337 0338 void RichTextEditor::forceAutoCorrection(bool selectedText) 0339 { 0340 Q_UNUSED(selectedText) 0341 // Nothing here 0342 } 0343 0344 void RichTextEditor::setSearchSupport(bool b) 0345 { 0346 if (b) { 0347 d->supportFeatures |= Search; 0348 } else { 0349 d->supportFeatures = (d->supportFeatures & ~Search); 0350 } 0351 } 0352 0353 bool RichTextEditor::searchSupport() const 0354 { 0355 return d->supportFeatures & Search; 0356 } 0357 0358 void RichTextEditor::setAllowTabSupport(bool b) 0359 { 0360 if (b) { 0361 d->supportFeatures |= AllowTab; 0362 } else { 0363 d->supportFeatures = (d->supportFeatures & ~AllowTab); 0364 } 0365 } 0366 0367 bool RichTextEditor::allowTabSupport() const 0368 { 0369 return d->supportFeatures & AllowTab; 0370 } 0371 0372 void RichTextEditor::setShowAutoCorrectButton(bool b) 0373 { 0374 d->showAutoCorrectionButton = b; 0375 } 0376 0377 bool RichTextEditor::showAutoCorrectButton() const 0378 { 0379 return d->showAutoCorrectionButton; 0380 } 0381 0382 bool RichTextEditor::spellCheckingSupport() const 0383 { 0384 return d->supportFeatures & SpellChecking; 0385 } 0386 0387 void RichTextEditor::setSpellCheckingSupport(bool check) 0388 { 0389 if (check) { 0390 d->supportFeatures |= SpellChecking; 0391 } else { 0392 d->supportFeatures = (d->supportFeatures & ~SpellChecking); 0393 } 0394 } 0395 0396 void RichTextEditor::setTextToSpeechSupport(bool b) 0397 { 0398 if (b) { 0399 d->supportFeatures |= TextToSpeech; 0400 } else { 0401 d->supportFeatures = (d->supportFeatures & ~TextToSpeech); 0402 } 0403 } 0404 0405 bool RichTextEditor::textToSpeechSupport() const 0406 { 0407 return d->supportFeatures & TextToSpeech; 0408 } 0409 0410 void RichTextEditor::slotAllowTab() 0411 { 0412 setTabChangesFocus(!tabChangesFocus()); 0413 } 0414 0415 void RichTextEditor::addExtraMenuEntry(QMenu *menu, QPoint pos) 0416 { 0417 Q_UNUSED(menu) 0418 Q_UNUSED(pos) 0419 } 0420 0421 void RichTextEditor::slotUndoableClear() 0422 { 0423 QTextCursor cursor = textCursor(); 0424 cursor.beginEditBlock(); 0425 cursor.movePosition(QTextCursor::Start); 0426 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); 0427 cursor.removeSelectedText(); 0428 cursor.endEditBlock(); 0429 } 0430 0431 void RichTextEditor::updateReadOnlyColor() 0432 { 0433 if (isReadOnly()) { 0434 QPalette p = palette(); 0435 p.setColor(QPalette::Base, d->mReadOnlyBackgroundColor); 0436 p.setColor(QPalette::Window, d->mReadOnlyBackgroundColor); 0437 setPalette(p); 0438 } 0439 } 0440 0441 void RichTextEditor::setReadOnly(bool readOnly) 0442 { 0443 if (!readOnly && hasFocus() && checkSpellingEnabled() && !d->richTextDecorator) { 0444 createHighlighter(); 0445 } 0446 0447 if (readOnly == isReadOnly()) { 0448 return; 0449 } 0450 0451 if (readOnly) { 0452 clearDecorator(); 0453 d->customPalette = testAttribute(Qt::WA_SetPalette); 0454 updateReadOnlyColor(); 0455 } else { 0456 if (d->customPalette && testAttribute(Qt::WA_SetPalette)) { 0457 QPalette p = palette(); 0458 QColor color = p.color(QPalette::Normal, QPalette::Base); 0459 p.setColor(QPalette::Base, color); 0460 p.setColor(QPalette::Window, color); 0461 setPalette(p); 0462 } else { 0463 setPalette(QPalette()); 0464 } 0465 } 0466 0467 QTextEdit::setReadOnly(readOnly); 0468 } 0469 0470 void RichTextEditor::checkSpelling(bool force) 0471 { 0472 if (document()->isEmpty()) { 0473 slotDisplayMessageIndicator(i18n("Nothing to spell check.")); 0474 if (force) { 0475 Q_EMIT spellCheckingFinished(); 0476 } 0477 return; 0478 } 0479 auto backgroundSpellCheck = new Sonnet::BackgroundChecker; 0480 if (backgroundSpellCheck->speller().availableBackends().isEmpty()) { 0481 if (force) { 0482 const int answer = KMessageBox::questionTwoActions(this, 0483 i18n("No backend available for spell checking. Do you want to send the email anyways?"), 0484 QString(), 0485 KGuiItem(i18nc("@action:button", "Send"), QStringLiteral("mail-send")), 0486 KStandardGuiItem::cancel()); 0487 if (answer == KMessageBox::ButtonCode::PrimaryAction) { 0488 Q_EMIT spellCheckingFinished(); 0489 } 0490 } else { 0491 slotDisplayMessageIndicator(i18n("No backend available for spell checking.")); 0492 } 0493 delete backgroundSpellCheck; 0494 return; 0495 } 0496 if (!d->spellCheckingLanguage.isEmpty()) { 0497 backgroundSpellCheck->changeLanguage(d->spellCheckingLanguage); 0498 } 0499 if (!d->ignoreSpellCheckingWords.isEmpty()) { 0500 for (const QString &word : std::as_const(d->ignoreSpellCheckingWords)) { 0501 backgroundSpellCheck->speller().addToSession(word); 0502 } 0503 } 0504 auto spellDialog = new Sonnet::Dialog(backgroundSpellCheck, force ? this : nullptr); 0505 auto buttonBox = spellDialog->findChild<QDialogButtonBox *>(); 0506 if (buttonBox) { 0507 auto skipButton = new QPushButton(i18n("Skip")); 0508 buttonBox->addButton(skipButton, QDialogButtonBox::ActionRole); 0509 connect(skipButton, &QPushButton::clicked, spellDialog, &Sonnet::Dialog::close); 0510 if (force) { 0511 connect(skipButton, &QPushButton::clicked, this, &RichTextEditor::spellCheckingFinished); 0512 } 0513 } else { 0514 qCWarning(TEXTCUSTOMEDITOR_LOG) << " Impossible to find qdialogbuttonbox"; 0515 } 0516 backgroundSpellCheck->setParent(spellDialog); 0517 spellDialog->setAttribute(Qt::WA_DeleteOnClose, true); 0518 spellDialog->activeAutoCorrect(d->showAutoCorrectionButton); 0519 connect(spellDialog, &Sonnet::Dialog::replace, this, &RichTextEditor::slotSpellCheckerCorrected); 0520 connect(spellDialog, &Sonnet::Dialog::misspelling, this, &RichTextEditor::slotSpellCheckerMisspelling); 0521 connect(spellDialog, &Sonnet::Dialog::autoCorrect, this, &RichTextEditor::slotSpellCheckerAutoCorrect); 0522 connect(spellDialog, &Sonnet::Dialog::spellCheckDone, this, &RichTextEditor::slotSpellCheckerFinished); 0523 connect(spellDialog, &Sonnet::Dialog::cancel, this, &RichTextEditor::slotSpellCheckerCanceled); 0524 connect(spellDialog, &Sonnet::Dialog::spellCheckStatus, this, &RichTextEditor::spellCheckStatus); 0525 connect(spellDialog, &Sonnet::Dialog::languageChanged, this, &RichTextEditor::languageChanged); 0526 if (force) { 0527 connect(spellDialog, &Sonnet::Dialog::spellCheckDone, this, &RichTextEditor::spellCheckingFinished); 0528 connect(spellDialog, &Sonnet::Dialog::cancel, this, &RichTextEditor::spellCheckingCanceled); 0529 } 0530 d->originalDoc = QTextDocumentFragment(document()); 0531 spellDialog->setBuffer(toPlainText()); 0532 spellDialog->show(); 0533 } 0534 0535 void RichTextEditor::slotCheckSpelling() 0536 { 0537 checkSpelling(false); 0538 } 0539 0540 void RichTextEditor::forceSpellChecking() 0541 { 0542 checkSpelling(true); 0543 } 0544 0545 void RichTextEditor::slotSpellCheckerCanceled() 0546 { 0547 QTextDocument *doc = document(); 0548 doc->clear(); 0549 QTextCursor cursor(doc); 0550 cursor.insertFragment(d->originalDoc); 0551 slotSpellCheckerFinished(); 0552 } 0553 0554 void RichTextEditor::slotSpellCheckerAutoCorrect(const QString ¤tWord, const QString &autoCorrectWord) 0555 { 0556 Q_EMIT spellCheckerAutoCorrect(currentWord, autoCorrectWord); 0557 } 0558 0559 void RichTextEditor::slotSpellCheckerMisspelling(const QString &text, int pos) 0560 { 0561 highlightWord(text.length(), pos); 0562 } 0563 0564 void RichTextEditor::slotSpellCheckerCorrected(const QString &oldWord, int pos, const QString &newWord) 0565 { 0566 if (oldWord != newWord) { 0567 QTextCursor cursor(document()); 0568 cursor.setPosition(pos); 0569 cursor.setPosition(pos + oldWord.length(), QTextCursor::KeepAnchor); 0570 cursor.insertText(newWord); 0571 } 0572 } 0573 0574 void RichTextEditor::slotSpellCheckerFinished() 0575 { 0576 QTextCursor cursor(document()); 0577 cursor.clearSelection(); 0578 setTextCursor(cursor); 0579 if (highlighter()) { 0580 highlighter()->rehighlight(); 0581 } 0582 } 0583 0584 void RichTextEditor::highlightWord(int length, int pos) 0585 { 0586 QTextCursor cursor(document()); 0587 cursor.setPosition(pos); 0588 cursor.setPosition(pos + length, QTextCursor::KeepAnchor); 0589 setTextCursor(cursor); 0590 ensureCursorVisible(); 0591 } 0592 0593 void RichTextEditor::createHighlighter() 0594 { 0595 auto highlighter = new Sonnet::Highlighter(this); 0596 highlighter->setCurrentLanguage(spellCheckingLanguage()); 0597 setHighlighter(highlighter); 0598 } 0599 0600 Sonnet::SpellCheckDecorator *RichTextEditor::createSpellCheckDecorator() 0601 { 0602 return new Sonnet::SpellCheckDecorator(this); 0603 } 0604 0605 void RichTextEditor::addIgnoreWordsToHighLighter() 0606 { 0607 if (d->ignoreSpellCheckingWords.isEmpty()) { 0608 return; 0609 } 0610 if (d->richTextDecorator) { 0611 Sonnet::Highlighter *_highlighter = d->richTextDecorator->highlighter(); 0612 for (const QString &word : std::as_const(d->ignoreSpellCheckingWords)) { 0613 _highlighter->ignoreWord(word); 0614 } 0615 } 0616 } 0617 0618 void RichTextEditor::setHighlighter(Sonnet::Highlighter *_highLighter) 0619 { 0620 Sonnet::SpellCheckDecorator *decorator = createSpellCheckDecorator(); 0621 delete decorator->highlighter(); 0622 decorator->setHighlighter(_highLighter); 0623 0624 d->richTextDecorator = decorator; 0625 addIgnoreWordsToHighLighter(); 0626 } 0627 0628 void RichTextEditor::focusInEvent(QFocusEvent *event) 0629 { 0630 if (d->checkSpellingEnabled && !isReadOnly() && !d->richTextDecorator && spellCheckingSupport()) { 0631 createHighlighter(); 0632 } 0633 0634 QTextEdit::focusInEvent(event); 0635 } 0636 0637 void RichTextEditor::setSpellCheckingConfigFileName(const QString &_fileName) 0638 { 0639 d->spellCheckingConfigFileName = _fileName; 0640 KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName); 0641 if (config->hasGroup(QLatin1String("Spelling"))) { 0642 KConfigGroup group(config, QLatin1String("Spelling")); 0643 d->checkSpellingEnabled = group.readEntry("checkerEnabledByDefault", false); 0644 d->spellCheckingLanguage = group.readEntry("Language", QString()); 0645 } 0646 setCheckSpellingEnabled(checkSpellingEnabled()); 0647 0648 if (!d->spellCheckingLanguage.isEmpty() && highlighter()) { 0649 highlighter()->setCurrentLanguage(d->spellCheckingLanguage); 0650 highlighter()->rehighlight(); 0651 } 0652 } 0653 0654 QString RichTextEditor::spellCheckingConfigFileName() const 0655 { 0656 return d->spellCheckingConfigFileName; 0657 } 0658 0659 bool RichTextEditor::checkSpellingEnabled() const 0660 { 0661 return d->checkSpellingEnabled; 0662 } 0663 0664 void RichTextEditor::setCheckSpellingEnabled(bool check) 0665 { 0666 if (check == d->checkSpellingEnabled) { 0667 return; 0668 } 0669 d->checkSpellingEnabled = check; 0670 Q_EMIT checkSpellingChanged(check); 0671 // From the above statement we know that if we're turning checking 0672 // on that we need to create a new highlighter and if we're turning it 0673 // off we should remove the old one. 0674 0675 if (check) { 0676 if (hasFocus()) { 0677 if (!d->richTextDecorator) { 0678 createHighlighter(); 0679 } 0680 if (!d->spellCheckingLanguage.isEmpty()) { 0681 setSpellCheckingLanguage(spellCheckingLanguage()); 0682 } 0683 } 0684 } else { 0685 clearDecorator(); 0686 } 0687 updateHighLighter(); 0688 } 0689 0690 void RichTextEditor::updateHighLighter() 0691 { 0692 } 0693 0694 void RichTextEditor::clearDecorator() 0695 { 0696 delete d->richTextDecorator; 0697 d->richTextDecorator = nullptr; 0698 } 0699 0700 const QString &RichTextEditor::spellCheckingLanguage() const 0701 { 0702 return d->spellCheckingLanguage; 0703 } 0704 0705 void RichTextEditor::setSpellCheckingLanguage(const QString &_language) 0706 { 0707 if (highlighter()) { 0708 highlighter()->setCurrentLanguage(_language); 0709 } 0710 0711 if (_language != d->spellCheckingLanguage) { 0712 d->spellCheckingLanguage = _language; 0713 KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName); 0714 KConfigGroup group(config, QLatin1String("Spelling")); 0715 group.writeEntry("Language", d->spellCheckingLanguage); 0716 0717 Q_EMIT languageChanged(_language); 0718 } 0719 } 0720 0721 void RichTextEditor::slotToggleAutoSpellCheck() 0722 { 0723 setCheckSpellingEnabled(!checkSpellingEnabled()); 0724 KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName); 0725 KConfigGroup group(config, QLatin1String("Spelling")); 0726 group.writeEntry("checkerEnabledByDefault", d->checkSpellingEnabled); 0727 } 0728 0729 void RichTextEditor::slotLanguageSelected() 0730 { 0731 auto languageAction = static_cast<QAction *>(QObject::sender()); 0732 setSpellCheckingLanguage(languageAction->data().toString()); 0733 } 0734 0735 static void deleteWord(QTextCursor cursor, QTextCursor::MoveOperation op) 0736 { 0737 cursor.clearSelection(); 0738 cursor.movePosition(op, QTextCursor::KeepAnchor); 0739 cursor.removeSelectedText(); 0740 } 0741 0742 void RichTextEditor::deleteWordBack() 0743 { 0744 deleteWord(textCursor(), QTextCursor::PreviousWord); 0745 } 0746 0747 void RichTextEditor::deleteWordForward() 0748 { 0749 deleteWord(textCursor(), QTextCursor::WordRight); 0750 } 0751 0752 bool RichTextEditor::event(QEvent *ev) 0753 { 0754 if (ev->type() == QEvent::ShortcutOverride) { 0755 auto e = static_cast<QKeyEvent *>(ev); 0756 if (overrideShortcut(e)) { 0757 e->accept(); 0758 return true; 0759 } 0760 } else if (ev->type() == QEvent::ApplicationPaletteChange) { 0761 regenerateColorScheme(); 0762 } 0763 return QTextEdit::event(ev); 0764 } 0765 0766 void RichTextEditor::wheelEvent(QWheelEvent *event) 0767 { 0768 if (QApplication::keyboardModifiers() & Qt::ControlModifier) { 0769 const int angleDeltaY{event->angleDelta().y()}; 0770 if (angleDeltaY > 0) { 0771 zoomIn(); 0772 } else if (angleDeltaY < 0) { 0773 zoomOut(); 0774 } 0775 event->accept(); 0776 return; 0777 } 0778 QTextEdit::wheelEvent(event); 0779 } 0780 0781 bool RichTextEditor::handleShortcut(QKeyEvent *event) 0782 { 0783 const int key = event->key() | event->modifiers(); 0784 0785 if (KStandardShortcut::copy().contains(key)) { 0786 copy(); 0787 return true; 0788 } else if (KStandardShortcut::paste().contains(key)) { 0789 paste(); 0790 return true; 0791 } else if (KStandardShortcut::cut().contains(key)) { 0792 cut(); 0793 return true; 0794 } else if (KStandardShortcut::undo().contains(key)) { 0795 if (!isReadOnly()) { 0796 undo(); 0797 } 0798 return true; 0799 } else if (KStandardShortcut::redo().contains(key)) { 0800 if (!isReadOnly()) { 0801 redo(); 0802 } 0803 return true; 0804 } else if (KStandardShortcut::deleteWordBack().contains(key)) { 0805 if (!isReadOnly()) { 0806 deleteWordBack(); 0807 } 0808 return true; 0809 } else if (KStandardShortcut::deleteWordForward().contains(key)) { 0810 if (!isReadOnly()) { 0811 deleteWordForward(); 0812 } 0813 return true; 0814 } else if (KStandardShortcut::backwardWord().contains(key)) { 0815 QTextCursor cursor = textCursor(); 0816 cursor.movePosition(QTextCursor::PreviousWord); 0817 setTextCursor(cursor); 0818 return true; 0819 } else if (KStandardShortcut::forwardWord().contains(key)) { 0820 QTextCursor cursor = textCursor(); 0821 cursor.movePosition(QTextCursor::NextWord); 0822 setTextCursor(cursor); 0823 return true; 0824 } else if (KStandardShortcut::next().contains(key)) { 0825 QTextCursor cursor = textCursor(); 0826 bool moved = false; 0827 qreal lastY = cursorRect(cursor).bottom(); 0828 qreal distance = 0; 0829 do { 0830 qreal y = cursorRect(cursor).bottom(); 0831 distance += qAbs(y - lastY); 0832 lastY = y; 0833 moved = cursor.movePosition(QTextCursor::Down); 0834 } while (moved && distance < viewport()->height()); 0835 0836 if (moved) { 0837 cursor.movePosition(QTextCursor::Up); 0838 verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd); 0839 } 0840 setTextCursor(cursor); 0841 return true; 0842 } else if (KStandardShortcut::prior().contains(key)) { 0843 QTextCursor cursor = textCursor(); 0844 bool moved = false; 0845 qreal lastY = cursorRect(cursor).bottom(); 0846 qreal distance = 0; 0847 do { 0848 qreal y = cursorRect(cursor).bottom(); 0849 distance += qAbs(y - lastY); 0850 lastY = y; 0851 moved = cursor.movePosition(QTextCursor::Up); 0852 } while (moved && distance < viewport()->height()); 0853 0854 if (moved) { 0855 cursor.movePosition(QTextCursor::Down); 0856 verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub); 0857 } 0858 setTextCursor(cursor); 0859 return true; 0860 } else if (KStandardShortcut::begin().contains(key)) { 0861 QTextCursor cursor = textCursor(); 0862 cursor.movePosition(QTextCursor::Start); 0863 setTextCursor(cursor); 0864 return true; 0865 } else if (KStandardShortcut::end().contains(key)) { 0866 QTextCursor cursor = textCursor(); 0867 cursor.movePosition(QTextCursor::End); 0868 setTextCursor(cursor); 0869 return true; 0870 } else if (KStandardShortcut::beginningOfLine().contains(key)) { 0871 QTextCursor cursor = textCursor(); 0872 cursor.movePosition(QTextCursor::StartOfLine); 0873 setTextCursor(cursor); 0874 return true; 0875 } else if (KStandardShortcut::endOfLine().contains(key)) { 0876 QTextCursor cursor = textCursor(); 0877 cursor.movePosition(QTextCursor::EndOfLine); 0878 setTextCursor(cursor); 0879 return true; 0880 } else if (searchSupport() && KStandardShortcut::find().contains(key)) { 0881 Q_EMIT findText(); 0882 return true; 0883 } else if (searchSupport() && KStandardShortcut::replace().contains(key)) { 0884 if (!isReadOnly()) { 0885 Q_EMIT replaceText(); 0886 } 0887 return true; 0888 } else if (KStandardShortcut::pasteSelection().contains(key)) { 0889 QString text = QApplication::clipboard()->text(QClipboard::Selection); 0890 if (!text.isEmpty()) { 0891 insertPlainText(text); // TODO: check if this is html? (MiB) 0892 } 0893 return true; 0894 } else if (event == QKeySequence::DeleteEndOfLine) { 0895 QTextCursor cursor = textCursor(); 0896 QTextBlock block = cursor.block(); 0897 if (cursor.position() == block.position() + block.length() - 2) { 0898 cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); 0899 } else { 0900 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 0901 } 0902 cursor.removeSelectedText(); 0903 setTextCursor(cursor); 0904 return true; 0905 } 0906 0907 return false; 0908 } 0909 0910 bool RichTextEditor::overrideShortcut(QKeyEvent *event) 0911 { 0912 const int key = event->key() | event->modifiers(); 0913 0914 if (KStandardShortcut::copy().contains(key)) { 0915 return true; 0916 } else if (KStandardShortcut::paste().contains(key)) { 0917 return true; 0918 } else if (KStandardShortcut::cut().contains(key)) { 0919 return true; 0920 } else if (KStandardShortcut::undo().contains(key)) { 0921 return true; 0922 } else if (KStandardShortcut::redo().contains(key)) { 0923 return true; 0924 } else if (KStandardShortcut::deleteWordBack().contains(key)) { 0925 return true; 0926 } else if (KStandardShortcut::deleteWordForward().contains(key)) { 0927 return true; 0928 } else if (KStandardShortcut::backwardWord().contains(key)) { 0929 return true; 0930 } else if (KStandardShortcut::forwardWord().contains(key)) { 0931 return true; 0932 } else if (KStandardShortcut::next().contains(key)) { 0933 return true; 0934 } else if (KStandardShortcut::prior().contains(key)) { 0935 return true; 0936 } else if (KStandardShortcut::begin().contains(key)) { 0937 return true; 0938 } else if (KStandardShortcut::end().contains(key)) { 0939 return true; 0940 } else if (KStandardShortcut::beginningOfLine().contains(key)) { 0941 return true; 0942 } else if (KStandardShortcut::endOfLine().contains(key)) { 0943 return true; 0944 } else if (KStandardShortcut::pasteSelection().contains(key)) { 0945 return true; 0946 } else if (searchSupport() && KStandardShortcut::find().contains(key)) { 0947 return true; 0948 } else if (searchSupport() && KStandardShortcut::findNext().contains(key)) { 0949 return true; 0950 } else if (searchSupport() && KStandardShortcut::replace().contains(key)) { 0951 return true; 0952 } else if (event->matches(QKeySequence::SelectAll)) { // currently missing in QTextEdit 0953 return true; 0954 } else if (event == QKeySequence::DeleteEndOfLine) { 0955 return true; 0956 } 0957 return false; 0958 } 0959 0960 void RichTextEditor::keyPressEvent(QKeyEvent *event) 0961 { 0962 const bool isControlClicked = event->modifiers() & Qt::ControlModifier; 0963 const bool isShiftClicked = event->modifiers() & Qt::ShiftModifier; 0964 if (handleShortcut(event)) { 0965 event->accept(); 0966 } else if (event->key() == Qt::Key_Up && isControlClicked && isShiftClicked) { 0967 moveLineUpDown(true); 0968 event->accept(); 0969 } else if (event->key() == Qt::Key_Down && isControlClicked && isShiftClicked) { 0970 moveLineUpDown(false); 0971 event->accept(); 0972 } else if (event->key() == Qt::Key_Up && isControlClicked) { 0973 moveCursorBeginUpDown(true); 0974 event->accept(); 0975 } else if (event->key() == Qt::Key_Down && isControlClicked) { 0976 moveCursorBeginUpDown(false); 0977 event->accept(); 0978 } else { 0979 QTextEdit::keyPressEvent(event); 0980 } 0981 } 0982 0983 int RichTextEditor::zoomFactor() const 0984 { 0985 int pourcentage = 100; 0986 const QFont f = font(); 0987 if (d->mInitialFontSize != f.pointSize()) { 0988 pourcentage = (f.pointSize() * 100) / d->mInitialFontSize; 0989 } 0990 return pourcentage; 0991 } 0992 0993 void RichTextEditor::slotZoomReset() 0994 { 0995 QFont f = font(); 0996 if (d->mInitialFontSize != f.pointSize()) { 0997 f.setPointSize(d->mInitialFontSize); 0998 setFont(f); 0999 } 1000 } 1001 1002 void RichTextEditor::moveCursorBeginUpDown(bool moveUp) 1003 { 1004 QTextCursor cursor = textCursor(); 1005 QTextCursor move = cursor; 1006 move.beginEditBlock(); 1007 cursor.clearSelection(); 1008 move.movePosition(QTextCursor::StartOfBlock); 1009 move.movePosition(moveUp ? QTextCursor::PreviousBlock : QTextCursor::NextBlock); 1010 move.endEditBlock(); 1011 setTextCursor(move); 1012 } 1013 1014 void RichTextEditor::moveLineUpDown(bool moveUp) 1015 { 1016 QTextCursor cursor = textCursor(); 1017 QTextCursor move = cursor; 1018 move.beginEditBlock(); 1019 1020 const bool hasSelection = cursor.hasSelection(); 1021 1022 if (hasSelection) { 1023 move.setPosition(cursor.selectionStart()); 1024 move.movePosition(QTextCursor::StartOfBlock); 1025 move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor); 1026 move.movePosition(move.atBlockStart() ? QTextCursor::Left : QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 1027 } else { 1028 move.movePosition(QTextCursor::StartOfBlock); 1029 move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 1030 } 1031 const QString text = move.selectedText(); 1032 1033 move.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); 1034 move.removeSelectedText(); 1035 1036 if (moveUp) { 1037 move.movePosition(QTextCursor::PreviousBlock); 1038 move.insertBlock(); 1039 move.movePosition(QTextCursor::Left); 1040 } else { 1041 move.movePosition(QTextCursor::EndOfBlock); 1042 if (move.atBlockStart()) { // empty block 1043 move.movePosition(QTextCursor::NextBlock); 1044 move.insertBlock(); 1045 move.movePosition(QTextCursor::Left); 1046 } else { 1047 move.insertBlock(); 1048 } 1049 } 1050 1051 int start = move.position(); 1052 move.clearSelection(); 1053 move.insertText(text); 1054 int end = move.position(); 1055 1056 if (hasSelection) { 1057 move.setPosition(end); 1058 move.setPosition(start, QTextCursor::KeepAnchor); 1059 } else { 1060 move.setPosition(start); 1061 } 1062 move.endEditBlock(); 1063 1064 setTextCursor(move); 1065 } 1066 1067 #include "moc_richtexteditor.cpp"