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 &currentWord, 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"