KTextAddons

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

KDE's Doxygen guidelines are available online.