KTextAddons

richtexteditor.cpp
1/*
2 SPDX-FileCopyrightText: 2013-2024 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "richtexteditor.h"
8using namespace Qt::Literals::StringLiterals;
9
10#include "textcustomeditor_debug.h"
11
12#include "widgets/textmessageindicator.h"
13#include <KConfig>
14#include <KConfigGroup>
15#include <KCursor>
16#include <KLocalizedString>
17#include <KMessageBox>
18#include <KSharedConfig>
19#include <KStandardActions>
20#include <KStandardGuiItem>
21#include <QActionGroup>
22#include <QIcon>
23
24#include "config-textcustomeditor.h"
25#if HAVE_KTEXTADDONS_KIO_SUPPORT
26#include <KIO/KUriFilterSearchProviderActions>
27#endif
28#include <Sonnet/Dialog>
29#include <Sonnet/Highlighter>
30#include <sonnet/backgroundchecker.h>
31#include <sonnet/spellcheckdecorator.h>
32#include <sonnet/speller.h>
33#if HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT
34#include <TextEditTextToSpeech/TextToSpeech>
35#endif
36#include <TextEmoticonsWidgets/EmoticonTextEditAction>
37
38#include <KColorScheme>
39#include <QApplication>
40#include <QClipboard>
41#include <QContextMenuEvent>
42#include <QDialogButtonBox>
43#include <QMenu>
44#include <QPushButton>
45#include <QScrollBar>
46#include <QTextCursor>
47#include <QTextDocumentFragment>
48
49using namespace TextCustomEditor;
50class Q_DECL_HIDDEN RichTextEditor::RichTextEditorPrivate
51{
52public:
53 RichTextEditorPrivate(RichTextEditor *qq)
54 : q(qq)
55 , textIndicator(new TextCustomEditor::TextMessageIndicator(q))
56#if HAVE_KTEXTADDONS_KIO_SUPPORT
57 , webshortcutMenuManager(new KIO::KUriFilterSearchProviderActions(q))
58#endif
59 {
60 KConfig sonnetKConfig(QStringLiteral("sonnetrc"));
61 KConfigGroup group(&sonnetKConfig, "Spelling"_L1);
62 checkSpellingEnabled = group.readEntry("checkerEnabledByDefault", false);
63 supportFeatures |= RichTextEditor::Search;
64 supportFeatures |= RichTextEditor::SpellChecking;
65 supportFeatures |= RichTextEditor::TextToSpeech;
66 supportFeatures |= RichTextEditor::AllowTab;
67#if HAVE_KTEXTADDONS_KIO_SUPPORT
68 supportFeatures |= RichTextEditor::AllowWebShortcut;
69#endif
70
71 // Workaround QTextEdit behavior: if the cursor points right after the link
72 // and start typing, the char format is kept. If user wants to write normal
73 // text right after the link, the only way is to move cursor at the next character
74 // (say for "<a>text</a>more text" the character has to be before letter "o"!)
75 // It's impossible if the whole document ends with a link.
76 // The same happens when text starts with a link: it's impossible to write normal text before it.
77 QObject::connect(q, &RichTextEditor::cursorPositionChanged, q, [this]() {
78 QTextCursor c = q->textCursor();
79 if (c.charFormat().isAnchor() && !c.hasSelection()) {
81 // If we are at block start or end (and at anchor), we just set the "default" format
82 if (!c.atBlockEnd() && !c.atBlockStart() && !c.hasSelection()) {
83 QTextCursor probe = c;
84 // Otherwise, if the next character is not a link, we just grab it's format
86 if (!probe.charFormat().isAnchor()) {
87 fmt = probe.charFormat();
88 }
89 }
90 c.setCharFormat(fmt);
91 q->setTextCursor(c);
92 }
93 });
94 }
95
96 ~RichTextEditorPrivate()
97 {
98 delete richTextDecorator;
99 delete speller;
100 }
101
102 QStringList ignoreSpellCheckingWords;
103 RichTextEditor *const q;
104 TextCustomEditor::TextMessageIndicator *const textIndicator;
105 QString spellCheckingConfigFileName;
106 QString spellCheckingLanguage;
107 QTextDocumentFragment originalDoc;
108 Sonnet::SpellCheckDecorator *richTextDecorator = nullptr;
109 Sonnet::Speller *speller = nullptr;
110#if HAVE_KTEXTADDONS_KIO_SUPPORT
111 KIO::KUriFilterSearchProviderActions *const webshortcutMenuManager;
112#endif
113 RichTextEditor::SupportFeatures supportFeatures;
114 QColor mReadOnlyBackgroundColor;
115 int mInitialFontSize;
116 bool customPalette = false;
117 bool checkSpellingEnabled = false;
118 bool activateLanguageMenu = true;
119 bool showAutoCorrectionButton = false;
120};
121
122RichTextEditor::RichTextEditor(QWidget *parent)
123 : QTextEdit(parent)
124 , d(new RichTextEditorPrivate(this))
125{
126 setAcceptRichText(true);
127 KCursor::setAutoHideCursor(this, true, false);
128 setSpellCheckingConfigFileName(QString());
129 d->mInitialFontSize = font().pointSize();
130 regenerateColorScheme();
131}
132
133RichTextEditor::~RichTextEditor() = default;
134
135void RichTextEditor::regenerateColorScheme()
136{
137 d->mReadOnlyBackgroundColor = KColorScheme(QPalette::Disabled, KColorScheme::View).background().color();
138 updateReadOnlyColor();
139}
140
141void RichTextEditor::setDefaultFontSize(int val)
142{
143 d->mInitialFontSize = val;
144 slotZoomReset();
145}
146
147void RichTextEditor::slotDisplayMessageIndicator(const QString &message)
148{
149 d->textIndicator->display(message);
150}
151
152Sonnet::Highlighter *RichTextEditor::highlighter() const
153{
154 if (d->richTextDecorator) {
155 return d->richTextDecorator->highlighter();
156 } else {
157 return nullptr;
158 }
159}
160
161bool RichTextEditor::activateLanguageMenu() const
162{
163 return d->activateLanguageMenu;
164}
165
166void RichTextEditor::setActivateLanguageMenu(bool activate)
167{
168 d->activateLanguageMenu = activate;
169}
170
171void RichTextEditor::contextMenuEvent(QContextMenuEvent *event)
172{
173 QMenu *popup = mousePopupMenu(event->pos());
174 if (popup) {
175 popup->exec(event->globalPos());
176 delete popup;
177 }
178}
179
180QMenu *RichTextEditor::mousePopupMenu(QPoint pos)
181{
183 if (popup) {
184 const bool emptyDocument = document()->isEmpty();
185 if (!isReadOnly()) {
186 const QList<QAction *> actionList = popup->actions();
187 enum {
188 UndoAct,
189 RedoAct,
190 CutAct,
191 CopyAct,
192 PasteAct,
193 ClearAct,
194 SelectAllAct,
195 NCountActs
196 };
197 QAction *separatorAction = nullptr;
198 const int idx = actionList.indexOf(actionList[SelectAllAct]) + 1;
199 if (idx < actionList.count()) {
200 separatorAction = actionList.at(idx);
201 }
202 if (separatorAction) {
203 QAction *clearAllAction = KStandardActions::clear(this, &RichTextEditor::slotUndoableClear, popup);
204 if (emptyDocument) {
205 clearAllAction->setEnabled(false);
206 }
207 popup->insertAction(separatorAction, clearAllAction);
208 }
209 }
210 if (searchSupport()) {
211 popup->addSeparator();
212 QAction *findAction = KStandardActions::find(this, &RichTextEditor::findText, popup);
213 popup->addAction(findAction);
214 if (emptyDocument) {
215 findAction->setEnabled(false);
216 }
217 popup->addSeparator();
218 if (!isReadOnly()) {
219 QAction *act = KStandardActions::replace(this, &RichTextEditor::replaceText, popup);
220 popup->addAction(act);
221 if (emptyDocument) {
222 act->setEnabled(false);
223 }
224 popup->addSeparator();
225 }
226 } else {
227 popup->addSeparator();
228 }
229
230 if (!isReadOnly() && spellCheckingSupport()) {
231 if (!d->speller) {
232 d->speller = new Sonnet::Speller();
233 }
234 if (!d->speller->availableBackends().isEmpty()) {
235 QAction *spellCheckAction = popup->addAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")),
236 i18n("Check Spelling…"),
237 this,
238 &RichTextEditor::slotCheckSpelling);
239 if (emptyDocument) {
240 spellCheckAction->setEnabled(false);
241 }
242 popup->addSeparator();
243 QAction *autoSpellCheckAction = popup->addAction(i18n("Auto Spell Check"), this, &RichTextEditor::slotToggleAutoSpellCheck);
244 autoSpellCheckAction->setCheckable(true);
245 autoSpellCheckAction->setChecked(checkSpellingEnabled());
246 popup->addAction(autoSpellCheckAction);
247
248 if (checkSpellingEnabled() && d->activateLanguageMenu) {
249 auto languagesMenu = new QMenu(i18n("Spell Checking Language"), popup);
250 auto languagesGroup = new QActionGroup(languagesMenu);
251 languagesGroup->setExclusive(true);
252
253 QString defaultSpellcheckingLanguage = spellCheckingLanguage();
254 if (defaultSpellcheckingLanguage.isEmpty()) {
255 defaultSpellcheckingLanguage = d->speller->defaultLanguage();
256 }
257
258 QMapIterator<QString, QString> i(d->speller->availableDictionaries());
259 while (i.hasNext()) {
260 i.next();
261 QAction *languageAction = languagesMenu->addAction(i.key());
262 languageAction->setCheckable(true);
263 languageAction->setChecked(defaultSpellcheckingLanguage == i.value());
264 languageAction->setData(i.value());
265 languageAction->setActionGroup(languagesGroup);
266 connect(languageAction, &QAction::triggered, this, &RichTextEditor::slotLanguageSelected);
267 }
268 popup->addMenu(languagesMenu);
269 }
270 popup->addSeparator();
271 }
272 }
273
274 if (allowTabSupport() && !isReadOnly()) {
275 QAction *allowTabAction = popup->addAction(i18n("Allow Tabulations"));
276 allowTabAction->setCheckable(true);
277 allowTabAction->setChecked(!tabChangesFocus());
278 connect(allowTabAction, &QAction::triggered, this, &RichTextEditor::slotAllowTab);
279 }
280#if HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT
281 if (!emptyDocument) {
282 QAction *speakAction = popup->addAction(i18n("Speak Text"));
283 speakAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech")));
284 connect(speakAction, &QAction::triggered, this, &RichTextEditor::slotSpeakText);
285 }
286#endif
287#if HAVE_KTEXTADDONS_KIO_SUPPORT
288 if (webShortcutSupport() && textCursor().hasSelection()) {
289 popup->addSeparator();
290 const QString selectedText = textCursor().selectedText();
291 d->webshortcutMenuManager->setSelectedText(selectedText);
292 d->webshortcutMenuManager->addWebShortcutsToMenu(popup);
293 }
294#endif
295 if (emojiSupport()) {
296 popup->addSeparator();
297 auto action = new TextEmoticonsWidgets::EmoticonTextEditAction(this);
298 popup->addAction(action);
299 connect(action, &TextEmoticonsWidgets::EmoticonTextEditAction::insertEmoticon, this, &RichTextEditor::slotInsertEmoticon);
300 }
301 addExtraMenuEntry(popup, pos);
302 return popup;
303 }
304 return nullptr;
305}
306
307void RichTextEditor::slotInsertEmoticon(const QString &str)
308{
309 insertPlainText(str);
310}
311
312void RichTextEditor::slotSpeakText()
313{
314 QString text;
315 if (textCursor().hasSelection()) {
316 text = textCursor().selectedText();
317 } else {
318 text = toPlainText();
319 }
320 Q_EMIT say(text);
321}
322
323void RichTextEditor::setWebShortcutSupport(bool b)
324{
325#if HAVE_KTEXTADDONS_KIO_SUPPORT
326 if (b) {
327 d->supportFeatures |= AllowWebShortcut;
328 } else {
329 d->supportFeatures = (d->supportFeatures & ~AllowWebShortcut);
330 }
331#else
332 Q_UNUSED(b);
333#endif
334}
335
336bool RichTextEditor::webShortcutSupport() const
337{
338#if HAVE_KTEXTADDONS_KIO_SUPPORT
339 return d->supportFeatures & AllowWebShortcut;
340#else
341 return false;
342#endif
343}
344
345void RichTextEditor::setEmojiSupport(bool b)
346{
347 if (b) {
348 d->supportFeatures |= Emoji;
349 } else {
350 d->supportFeatures = (d->supportFeatures & ~Emoji);
351 }
352}
353
354bool RichTextEditor::emojiSupport() const
355{
356 return d->supportFeatures & Emoji;
357}
358
359void RichTextEditor::addIgnoreWords(const QStringList &lst)
360{
361 d->ignoreSpellCheckingWords = lst;
362 addIgnoreWordsToHighLighter();
363}
364
365void RichTextEditor::forceAutoCorrection(bool selectedText)
366{
367 Q_UNUSED(selectedText)
368 // Nothing here
369}
370
371void RichTextEditor::setSearchSupport(bool b)
372{
373 if (b) {
374 d->supportFeatures |= Search;
375 } else {
376 d->supportFeatures = (d->supportFeatures & ~Search);
377 }
378}
379
380bool RichTextEditor::searchSupport() const
381{
382 return d->supportFeatures & Search;
383}
384
385void RichTextEditor::setAllowTabSupport(bool b)
386{
387 if (b) {
388 d->supportFeatures |= AllowTab;
389 } else {
390 d->supportFeatures = (d->supportFeatures & ~AllowTab);
391 }
392}
393
394bool RichTextEditor::allowTabSupport() const
395{
396 return d->supportFeatures & AllowTab;
397}
398
399void RichTextEditor::setShowAutoCorrectButton(bool b)
400{
401 d->showAutoCorrectionButton = b;
402}
403
404bool RichTextEditor::showAutoCorrectButton() const
405{
406 return d->showAutoCorrectionButton;
407}
408
409bool RichTextEditor::spellCheckingSupport() const
410{
411 return d->supportFeatures & SpellChecking;
412}
413
414void RichTextEditor::setSpellCheckingSupport(bool check)
415{
416 if (check) {
417 d->supportFeatures |= SpellChecking;
418 } else {
419 d->supportFeatures = (d->supportFeatures & ~SpellChecking);
420 }
421}
422
423void RichTextEditor::setTextToSpeechSupport(bool b)
424{
425 if (b) {
426 d->supportFeatures |= TextToSpeech;
427 } else {
428 d->supportFeatures = (d->supportFeatures & ~TextToSpeech);
429 }
430}
431
432bool RichTextEditor::textToSpeechSupport() const
433{
434 return d->supportFeatures & TextToSpeech;
435}
436
437void RichTextEditor::slotAllowTab()
438{
440}
441
442void RichTextEditor::addExtraMenuEntry(QMenu *menu, QPoint pos)
443{
444 Q_UNUSED(menu)
445 Q_UNUSED(pos)
446}
447
448void RichTextEditor::slotUndoableClear()
449{
451 cursor.beginEditBlock();
452 cursor.movePosition(QTextCursor::Start);
454 cursor.removeSelectedText();
455 cursor.endEditBlock();
456}
457
458void RichTextEditor::updateReadOnlyColor()
459{
460 if (isReadOnly()) {
461 QPalette p = palette();
462 p.setColor(QPalette::Base, d->mReadOnlyBackgroundColor);
463 p.setColor(QPalette::Window, d->mReadOnlyBackgroundColor);
464 setPalette(p);
465 }
466}
467
468void RichTextEditor::setReadOnly(bool readOnly)
469{
470 if (!readOnly && hasFocus() && checkSpellingEnabled() && !d->richTextDecorator) {
471 createHighlighter();
472 }
473
474 if (readOnly == isReadOnly()) {
475 return;
476 }
477
478 if (readOnly) {
479 clearDecorator();
480 d->customPalette = testAttribute(Qt::WA_SetPalette);
481 updateReadOnlyColor();
482 } else {
483 if (d->customPalette && testAttribute(Qt::WA_SetPalette)) {
484 QPalette p = palette();
486 p.setColor(QPalette::Base, color);
487 p.setColor(QPalette::Window, color);
488 setPalette(p);
489 } else {
491 }
492 }
493
495}
496
497void RichTextEditor::checkSpelling(bool force)
498{
499 if (document()->isEmpty()) {
500 slotDisplayMessageIndicator(i18n("Nothing to spell check."));
501 if (force) {
502 Q_EMIT spellCheckingFinished();
503 }
504 return;
505 }
506 auto backgroundSpellCheck = new Sonnet::BackgroundChecker;
507 if (backgroundSpellCheck->speller().availableBackends().isEmpty()) {
508 if (force) {
509 const int answer = KMessageBox::questionTwoActions(this,
510 i18n("No backend available for spell checking. Do you want to send the email anyways?"),
511 QString(),
512 KGuiItem(i18nc("@action:button", "Send"), QStringLiteral("mail-send")),
514 if (answer == KMessageBox::ButtonCode::PrimaryAction) {
515 Q_EMIT spellCheckingFinished();
516 }
517 } else {
518 slotDisplayMessageIndicator(i18n("No backend available for spell checking."));
519 }
520 delete backgroundSpellCheck;
521 return;
522 }
523 if (!d->spellCheckingLanguage.isEmpty()) {
524 backgroundSpellCheck->changeLanguage(d->spellCheckingLanguage);
525 }
526 if (!d->ignoreSpellCheckingWords.isEmpty()) {
527 for (const QString &word : std::as_const(d->ignoreSpellCheckingWords)) {
528 backgroundSpellCheck->speller().addToSession(word);
529 }
530 }
531 auto spellDialog = new Sonnet::Dialog(backgroundSpellCheck, force ? this : nullptr);
532 auto buttonBox = spellDialog->findChild<QDialogButtonBox *>();
533 if (buttonBox) {
534 auto skipButton = new QPushButton(i18nc("@action:button", "Skip"));
535 buttonBox->addButton(skipButton, QDialogButtonBox::ActionRole);
536 connect(skipButton, &QPushButton::clicked, spellDialog, &Sonnet::Dialog::close);
537 if (force) {
538 connect(skipButton, &QPushButton::clicked, this, &RichTextEditor::spellCheckingFinished);
539 }
540 } else {
541 qCWarning(TEXTCUSTOMEDITOR_LOG) << " Impossible to find qdialogbuttonbox";
542 }
543 backgroundSpellCheck->setParent(spellDialog);
544 spellDialog->setAttribute(Qt::WA_DeleteOnClose, true);
545 spellDialog->activeAutoCorrect(d->showAutoCorrectionButton);
546 connect(spellDialog, &Sonnet::Dialog::replace, this, &RichTextEditor::slotSpellCheckerCorrected);
547 connect(spellDialog, &Sonnet::Dialog::misspelling, this, &RichTextEditor::slotSpellCheckerMisspelling);
548 connect(spellDialog, &Sonnet::Dialog::autoCorrect, this, &RichTextEditor::slotSpellCheckerAutoCorrect);
549 connect(spellDialog, &Sonnet::Dialog::spellCheckDone, this, &RichTextEditor::slotSpellCheckerFinished);
550 connect(spellDialog, &Sonnet::Dialog::cancel, this, &RichTextEditor::slotSpellCheckerCanceled);
551 connect(spellDialog, &Sonnet::Dialog::spellCheckStatus, this, &RichTextEditor::spellCheckStatus);
552 connect(spellDialog, &Sonnet::Dialog::languageChanged, this, &RichTextEditor::languageChanged);
553 if (force) {
554 connect(spellDialog, &Sonnet::Dialog::spellCheckDone, this, &RichTextEditor::spellCheckingFinished);
555 connect(spellDialog, &Sonnet::Dialog::cancel, this, &RichTextEditor::spellCheckingCanceled);
556 }
557 d->originalDoc = QTextDocumentFragment(document());
558 spellDialog->setBuffer(toPlainText());
559 spellDialog->show();
560}
561
562void RichTextEditor::slotCheckSpelling()
563{
564 checkSpelling(false);
565}
566
567void RichTextEditor::forceSpellChecking()
568{
569 checkSpelling(true);
570}
571
572void RichTextEditor::slotSpellCheckerCanceled()
573{
574 QTextDocument *doc = document();
575 doc->clear();
576 QTextCursor cursor(doc);
577 cursor.insertFragment(d->originalDoc);
578 slotSpellCheckerFinished();
579}
580
581void RichTextEditor::slotSpellCheckerAutoCorrect(const QString &currentWord, const QString &autoCorrectWord)
582{
583 Q_EMIT spellCheckerAutoCorrect(currentWord, autoCorrectWord);
584}
585
586void RichTextEditor::slotSpellCheckerMisspelling(const QString &text, int pos)
587{
588 highlightWord(text.length(), pos);
589}
590
591void RichTextEditor::slotSpellCheckerCorrected(const QString &oldWord, int pos, const QString &newWord)
592{
593 if (oldWord != newWord) {
595 cursor.setPosition(pos);
596 cursor.setPosition(pos + oldWord.length(), QTextCursor::KeepAnchor);
597 cursor.insertText(newWord);
598 }
599}
600
601void RichTextEditor::slotSpellCheckerFinished()
602{
604 cursor.clearSelection();
606 if (highlighter()) {
607 highlighter()->rehighlight();
608 }
609}
610
611void RichTextEditor::highlightWord(int length, int pos)
612{
614 cursor.setPosition(pos);
615 cursor.setPosition(pos + length, QTextCursor::KeepAnchor);
618}
619
620void RichTextEditor::createHighlighter()
621{
622 auto highlighter = new Sonnet::Highlighter(this);
623 highlighter->setCurrentLanguage(spellCheckingLanguage());
624 setHighlighter(highlighter);
625}
626
627Sonnet::SpellCheckDecorator *RichTextEditor::createSpellCheckDecorator()
628{
629 return new Sonnet::SpellCheckDecorator(this);
630}
631
632void RichTextEditor::addIgnoreWordsToHighLighter()
633{
634 if (d->ignoreSpellCheckingWords.isEmpty()) {
635 return;
636 }
637 if (d->richTextDecorator) {
638 Sonnet::Highlighter *_highlighter = d->richTextDecorator->highlighter();
639 for (const QString &word : std::as_const(d->ignoreSpellCheckingWords)) {
640 _highlighter->ignoreWord(word);
641 }
642 }
643}
644
645void RichTextEditor::setHighlighter(Sonnet::Highlighter *_highLighter)
646{
647 Sonnet::SpellCheckDecorator *decorator = createSpellCheckDecorator();
648 delete decorator->highlighter();
649 decorator->setHighlighter(_highLighter);
650
651 d->richTextDecorator = decorator;
652 addIgnoreWordsToHighLighter();
653}
654
655void RichTextEditor::focusInEvent(QFocusEvent *event)
656{
657 if (d->checkSpellingEnabled && !isReadOnly() && !d->richTextDecorator && spellCheckingSupport()) {
658 createHighlighter();
659 }
660
662}
663
664void RichTextEditor::setSpellCheckingConfigFileName(const QString &_fileName)
665{
666 d->spellCheckingConfigFileName = _fileName;
667 KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName);
668 if (config->hasGroup("Spelling"_L1)) {
669 KConfigGroup group(config, "Spelling"_L1);
670 d->checkSpellingEnabled = group.readEntry("checkerEnabledByDefault", false);
671 d->spellCheckingLanguage = group.readEntry("Language", QString());
672 }
673 setCheckSpellingEnabled(checkSpellingEnabled());
674
675 if (!d->spellCheckingLanguage.isEmpty() && highlighter()) {
676 highlighter()->setCurrentLanguage(d->spellCheckingLanguage);
677 highlighter()->rehighlight();
678 }
679}
680
681QString RichTextEditor::spellCheckingConfigFileName() const
682{
683 return d->spellCheckingConfigFileName;
684}
685
686bool RichTextEditor::checkSpellingEnabled() const
687{
688 return d->checkSpellingEnabled;
689}
690
691void RichTextEditor::setCheckSpellingEnabled(bool check)
692{
693 if (check == d->checkSpellingEnabled) {
694 return;
695 }
696 d->checkSpellingEnabled = check;
697 Q_EMIT checkSpellingChanged(check);
698 // From the above statement we know that if we're turning checking
699 // on that we need to create a new highlighter and if we're turning it
700 // off we should remove the old one.
701
702 if (check) {
703 if (hasFocus()) {
704 if (!d->richTextDecorator) {
705 createHighlighter();
706 }
707 if (!d->spellCheckingLanguage.isEmpty()) {
708 setSpellCheckingLanguage(spellCheckingLanguage());
709 }
710 }
711 } else {
712 clearDecorator();
713 }
714 updateHighLighter();
715}
716
717void RichTextEditor::updateHighLighter()
718{
719}
720
721void RichTextEditor::clearDecorator()
722{
723 delete d->richTextDecorator;
724 d->richTextDecorator = nullptr;
725}
726
727const QString &RichTextEditor::spellCheckingLanguage() const
728{
729 return d->spellCheckingLanguage;
730}
731
732void RichTextEditor::setSpellCheckingLanguage(const QString &_language)
733{
734 if (highlighter()) {
735 highlighter()->setCurrentLanguage(_language);
736 }
737
738 if (_language != d->spellCheckingLanguage) {
739 d->spellCheckingLanguage = _language;
740 KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName);
741 KConfigGroup group(config, "Spelling"_L1);
742 group.writeEntry("Language", d->spellCheckingLanguage);
743
744 Q_EMIT languageChanged(_language);
745 }
746}
747
748void RichTextEditor::slotToggleAutoSpellCheck()
749{
750 setCheckSpellingEnabled(!checkSpellingEnabled());
751 KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName);
752 KConfigGroup group(config, "Spelling"_L1);
753 group.writeEntry("checkerEnabledByDefault", d->checkSpellingEnabled);
754}
755
756void RichTextEditor::slotLanguageSelected()
757{
758 auto languageAction = static_cast<QAction *>(QObject::sender());
759 setSpellCheckingLanguage(languageAction->data().toString());
760}
761
762static void deleteWord(QTextCursor cursor, QTextCursor::MoveOperation op)
763{
764 cursor.clearSelection();
766 cursor.removeSelectedText();
767}
768
769void RichTextEditor::deleteWordBack()
770{
772}
773
774void RichTextEditor::deleteWordForward()
775{
776 deleteWord(textCursor(), QTextCursor::WordRight);
777}
778
779bool RichTextEditor::event(QEvent *ev)
780{
781 if (ev->type() == QEvent::ShortcutOverride) {
782 auto e = static_cast<QKeyEvent *>(ev);
783 if (overrideShortcut(e)) {
784 e->accept();
785 return true;
786 }
787 } else if (ev->type() == QEvent::ApplicationPaletteChange) {
788 regenerateColorScheme();
789 }
790 return QTextEdit::event(ev);
791}
792
793void RichTextEditor::wheelEvent(QWheelEvent *event)
794{
796 const int angleDeltaY{event->angleDelta().y()};
797 if (angleDeltaY > 0) {
798 zoomIn();
799 } else if (angleDeltaY < 0) {
800 zoomOut();
801 }
802 event->accept();
803 return;
804 }
806}
807
808bool RichTextEditor::handleShortcut(QKeyEvent *event)
809{
810 const int key = event->key() | event->modifiers();
811
812 if (KStandardShortcut::copy().contains(key)) {
813 copy();
814 return true;
815 } else if (KStandardShortcut::paste().contains(key)) {
816 paste();
817 return true;
818 } else if (KStandardShortcut::cut().contains(key)) {
819 cut();
820 return true;
821 } else if (KStandardShortcut::undo().contains(key)) {
822 if (!isReadOnly()) {
823 undo();
824 }
825 return true;
826 } else if (KStandardShortcut::redo().contains(key)) {
827 if (!isReadOnly()) {
828 redo();
829 }
830 return true;
831 } else if (KStandardShortcut::deleteWordBack().contains(key)) {
832 if (!isReadOnly()) {
833 deleteWordBack();
834 }
835 return true;
836 } else if (KStandardShortcut::deleteWordForward().contains(key)) {
837 if (!isReadOnly()) {
838 deleteWordForward();
839 }
840 return true;
841 } else if (KStandardShortcut::backwardWord().contains(key)) {
843 cursor.movePosition(QTextCursor::PreviousWord);
845 return true;
846 } else if (KStandardShortcut::forwardWord().contains(key)) {
848 cursor.movePosition(QTextCursor::NextWord);
850 return true;
851 } else if (KStandardShortcut::next().contains(key)) {
853 bool moved = false;
854 qreal lastY = cursorRect(cursor).bottom();
855 qreal distance = 0;
856 do {
857 qreal y = cursorRect(cursor).bottom();
858 distance += qAbs(y - lastY);
859 lastY = y;
860 moved = cursor.movePosition(QTextCursor::Down);
861 } while (moved && distance < viewport()->height());
862
863 if (moved) {
864 cursor.movePosition(QTextCursor::Up);
866 }
868 return true;
869 } else if (KStandardShortcut::prior().contains(key)) {
871 bool moved = false;
872 qreal lastY = cursorRect(cursor).bottom();
873 qreal distance = 0;
874 do {
875 qreal y = cursorRect(cursor).bottom();
876 distance += qAbs(y - lastY);
877 lastY = y;
878 moved = cursor.movePosition(QTextCursor::Up);
879 } while (moved && distance < viewport()->height());
880
881 if (moved) {
882 cursor.movePosition(QTextCursor::Down);
884 }
886 return true;
887 } else if (KStandardShortcut::begin().contains(key)) {
889 cursor.movePosition(QTextCursor::Start);
891 return true;
892 } else if (KStandardShortcut::end().contains(key)) {
894 cursor.movePosition(QTextCursor::End);
896 return true;
897 } else if (KStandardShortcut::beginningOfLine().contains(key)) {
899 cursor.movePosition(QTextCursor::StartOfLine);
901 return true;
902 } else if (KStandardShortcut::endOfLine().contains(key)) {
904 cursor.movePosition(QTextCursor::EndOfLine);
906 return true;
907 } else if (searchSupport() && KStandardShortcut::find().contains(key)) {
908 Q_EMIT findText();
909 return true;
910 } else if (searchSupport() && KStandardShortcut::replace().contains(key)) {
911 if (!isReadOnly()) {
912 Q_EMIT replaceText();
913 }
914 return true;
915 } else if (KStandardShortcut::pasteSelection().contains(key)) {
917 if (!text.isEmpty()) {
918 insertPlainText(text); // TODO: check if this is html? (MiB)
919 }
920 return true;
921 } else if (event == QKeySequence::DeleteEndOfLine) {
923 QTextBlock block = cursor.block();
924 if (cursor.position() == block.position() + block.length() - 2) {
926 } else {
928 }
929 cursor.removeSelectedText();
931 return true;
932 }
933
934 return false;
935}
936
937bool RichTextEditor::overrideShortcut(QKeyEvent *event)
938{
939 const int key = event->key() | event->modifiers();
940
941 if (KStandardShortcut::copy().contains(key)) {
942 return true;
943 } else if (KStandardShortcut::paste().contains(key)) {
944 return true;
945 } else if (KStandardShortcut::cut().contains(key)) {
946 return true;
947 } else if (KStandardShortcut::undo().contains(key)) {
948 return true;
949 } else if (KStandardShortcut::redo().contains(key)) {
950 return true;
951 } else if (KStandardShortcut::deleteWordBack().contains(key)) {
952 return true;
953 } else if (KStandardShortcut::deleteWordForward().contains(key)) {
954 return true;
955 } else if (KStandardShortcut::backwardWord().contains(key)) {
956 return true;
957 } else if (KStandardShortcut::forwardWord().contains(key)) {
958 return true;
959 } else if (KStandardShortcut::next().contains(key)) {
960 return true;
961 } else if (KStandardShortcut::prior().contains(key)) {
962 return true;
963 } else if (KStandardShortcut::begin().contains(key)) {
964 return true;
965 } else if (KStandardShortcut::end().contains(key)) {
966 return true;
967 } else if (KStandardShortcut::beginningOfLine().contains(key)) {
968 return true;
969 } else if (KStandardShortcut::endOfLine().contains(key)) {
970 return true;
971 } else if (KStandardShortcut::pasteSelection().contains(key)) {
972 return true;
973 } else if (searchSupport() && KStandardShortcut::find().contains(key)) {
974 return true;
975 } else if (searchSupport() && KStandardShortcut::findNext().contains(key)) {
976 return true;
977 } else if (searchSupport() && KStandardShortcut::replace().contains(key)) {
978 return true;
979 } else if (event->matches(QKeySequence::SelectAll)) { // currently missing in QTextEdit
980 return true;
981 } else if (event == QKeySequence::DeleteEndOfLine) {
982 return true;
983 }
984 return false;
985}
986
987void RichTextEditor::keyPressEvent(QKeyEvent *event)
988{
989 const bool isControlClicked = event->modifiers() & Qt::ControlModifier;
990 const bool isShiftClicked = event->modifiers() & Qt::ShiftModifier;
991 if (handleShortcut(event)) {
992 event->accept();
993 } else if (event->key() == Qt::Key_Up && isControlClicked && isShiftClicked) {
994 moveLineUpDown(true);
995 event->accept();
996 } else if (event->key() == Qt::Key_Down && isControlClicked && isShiftClicked) {
997 moveLineUpDown(false);
998 event->accept();
999 } else if (event->key() == Qt::Key_Up && isControlClicked) {
1000 moveCursorBeginUpDown(true);
1001 event->accept();
1002 } else if (event->key() == Qt::Key_Down && isControlClicked) {
1003 moveCursorBeginUpDown(false);
1004 event->accept();
1005 } else {
1007 }
1008}
1009
1010int RichTextEditor::zoomFactor() const
1011{
1012 int pourcentage = 100;
1013 const QFont f = font();
1014 if (d->mInitialFontSize != f.pointSize()) {
1015 pourcentage = (f.pointSize() * 100) / d->mInitialFontSize;
1016 }
1017 return pourcentage;
1018}
1019
1020void RichTextEditor::slotZoomReset()
1021{
1022 QFont f = font();
1023 if (d->mInitialFontSize != f.pointSize()) {
1024 f.setPointSize(d->mInitialFontSize);
1025 setFont(f);
1026 }
1027}
1028
1029void RichTextEditor::moveCursorBeginUpDown(bool moveUp)
1030{
1034 cursor.clearSelection();
1035 move.movePosition(QTextCursor::StartOfBlock);
1037 move.endEditBlock();
1039}
1040
1041void RichTextEditor::moveLineUpDown(bool moveUp)
1042{
1046
1047 const bool hasSelection = cursor.hasSelection();
1048
1049 if (hasSelection) {
1050 move.setPosition(cursor.selectionStart());
1051 move.movePosition(QTextCursor::StartOfBlock);
1052 move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
1054 } else {
1055 move.movePosition(QTextCursor::StartOfBlock);
1057 }
1058 const QString text = move.selectedText();
1059
1061 move.removeSelectedText();
1062
1063 if (moveUp) {
1064 move.movePosition(QTextCursor::PreviousBlock);
1065 move.insertBlock();
1066 move.movePosition(QTextCursor::Left);
1067 } else {
1068 move.movePosition(QTextCursor::EndOfBlock);
1069 if (move.atBlockStart()) { // empty block
1070 move.movePosition(QTextCursor::NextBlock);
1071 move.insertBlock();
1072 move.movePosition(QTextCursor::Left);
1073 } else {
1074 move.insertBlock();
1075 }
1076 }
1077
1078 int start = move.position();
1079 move.clearSelection();
1080 move.insertText(text);
1081 int end = move.position();
1082
1083 if (hasSelection) {
1084 move.setPosition(end);
1085 move.setPosition(start, QTextCursor::KeepAnchor);
1086 } else {
1087 move.setPosition(start);
1088 }
1089 move.endEditBlock();
1090
1092}
1093
1094#include "moc_richtexteditor.cpp"
QBrush background(BackgroundRole=NormalBackground) const
static void setAutoHideCursor(QWidget *w, bool enable, bool customEventFilter=false)
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
void spellCheckDone(const QString &newBuffer)
void spellCheckStatus(const QString &)
void languageChanged(const QString &language)
void ignoreWord(const QString &word)
void setCurrentLanguage(const QString &language)
Highlighter * highlighter() const
void setHighlighter(Highlighter *highlighter)
The RichTextEditor class.
A widget that displays messages in the top-left corner.
The TextEmoticonsWidgets::EmoticonTextEditAction class.
void insertEmoticon(const QString &)
This signal is emitted each time the user selects an emoji.
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
ButtonCode questionTwoActions(QWidget *parent, const QString &text, const QString &title, const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const QString &dontAskAgainName=QString(), Options options=Notify)
KGuiItem cancel()
const QList< QKeySequence > & beginningOfLine()
const QList< QKeySequence > & begin()
const QList< QKeySequence > & cut()
const QList< QKeySequence > & undo()
const QList< QKeySequence > & next()
const QList< QKeySequence > & deleteWordBack()
const QList< QKeySequence > & find()
const QList< QKeySequence > & paste()
const QList< QKeySequence > & end()
const QList< QKeySequence > & copy()
const QList< QKeySequence > & backwardWord()
const QList< QKeySequence > & endOfLine()
const QList< QKeySequence > & forwardWord()
const QList< QKeySequence > & deleteWordForward()
const QList< QKeySequence > & findNext()
const QList< QKeySequence > & prior()
const QList< QKeySequence > & replace()
const QList< QKeySequence > & pasteSelection()
const QList< QKeySequence > & redo()
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
void clicked(bool checked)
virtual bool event(QEvent *event) override
QScrollBar * verticalScrollBar() const const
QWidget * viewport() const const
void triggerAction(SliderAction action)
void setCheckable(bool)
void setChecked(bool)
QVariant data() const const
void setEnabled(bool)
void setIcon(const QIcon &icon)
void setActionGroup(QActionGroup *group)
void setData(const QVariant &data)
void triggered(bool checked)
const QColor & color() const const
QString text(Mode mode) const const
ShortcutOverride
Type type() const const
int pointSize() const const
void setPointSize(int pointSize)
QClipboard * clipboard()
Qt::KeyboardModifiers keyboardModifiers()
QIcon fromTheme(const QString &name)
const_reference at(qsizetype i) const const
qsizetype count() const const
qsizetype indexOf(const AT &value, qsizetype from) const const
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addMenu(QMenu *menu)
QAction * addSeparator()
QAction * exec()
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * sender() const const
const QColor & color(ColorGroup group, ColorRole role) const const
void setColor(ColorGroup group, ColorRole role, const QColor &color)
int bottom() const const
bool isEmpty() const const
qsizetype length() const const
ControlModifier
WA_SetPalette
int length() const const
int position() const const
bool isAnchor() const const
bool atBlockEnd() const const
bool atBlockStart() const const
void beginEditBlock()
QTextCharFormat charFormat() const const
void clearSelection()
bool hasSelection() const const
bool movePosition(MoveOperation operation, MoveMode mode, int n)
void removeSelectedText()
QString selectedText() const const
void setCharFormat(const QTextCharFormat &format)
virtual void clear()
void copy()
QMenu * createStandardContextMenu()
QRect cursorRect() const const
void cut()
void ensureCursorVisible()
virtual void focusInEvent(QFocusEvent *e) override
void insertPlainText(const QString &text)
virtual void keyPressEvent(QKeyEvent *e) override
void paste()
bool isReadOnly() const const
void redo()
void setTextCursor(const QTextCursor &cursor)
QTextCursor textCursor() const const
QString toPlainText() const const
void undo()
virtual void wheelEvent(QWheelEvent *e) override
void zoomIn(int range)
void zoomOut(int range)
QString toString() const const
QList< QAction * > actions() const const
bool close()
bool hasFocus() const const
void insertAction(QAction *before, QAction *action)
void setParent(QWidget *parent)
bool testAttribute(Qt::WidgetAttribute attribute) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:02:02 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.