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 <KStandardAction>
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 { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs };
188 QAction *separatorAction = nullptr;
189 const int idx = actionList.indexOf(actionList[SelectAllAct]) + 1;
190 if (idx < actionList.count()) {
191 separatorAction = actionList.at(idx);
192 }
193 if (separatorAction) {
194 QAction *clearAllAction = KStandardAction::clear(this, &RichTextEditor::slotUndoableClear, popup);
195 if (emptyDocument) {
196 clearAllAction->setEnabled(false);
197 }
198 popup->insertAction(separatorAction, clearAllAction);
199 }
200 }
201 if (searchSupport()) {
202 popup->addSeparator();
203 QAction *findAction = KStandardAction::find(this, &RichTextEditor::findText, popup);
204 popup->addAction(findAction);
205 if (emptyDocument) {
206 findAction->setEnabled(false);
207 }
208 popup->addSeparator();
209 if (!isReadOnly()) {
210 QAction *act = KStandardAction::replace(this, &RichTextEditor::replaceText, popup);
211 popup->addAction(act);
212 if (emptyDocument) {
213 act->setEnabled(false);
214 }
215 popup->addSeparator();
216 }
217 } else {
218 popup->addSeparator();
219 }
220
221 if (!isReadOnly() && spellCheckingSupport()) {
222 if (!d->speller) {
223 d->speller = new Sonnet::Speller();
224 }
225 if (!d->speller->availableBackends().isEmpty()) {
226 QAction *spellCheckAction = popup->addAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")),
227 i18n("Check Spelling…"),
228 this,
229 &RichTextEditor::slotCheckSpelling);
230 if (emptyDocument) {
231 spellCheckAction->setEnabled(false);
232 }
233 popup->addSeparator();
234 QAction *autoSpellCheckAction = popup->addAction(i18n("Auto Spell Check"), this, &RichTextEditor::slotToggleAutoSpellCheck);
235 autoSpellCheckAction->setCheckable(true);
236 autoSpellCheckAction->setChecked(checkSpellingEnabled());
237 popup->addAction(autoSpellCheckAction);
238
239 if (checkSpellingEnabled() && d->activateLanguageMenu) {
240 auto languagesMenu = new QMenu(i18n("Spell Checking Language"), popup);
241 auto languagesGroup = new QActionGroup(languagesMenu);
242 languagesGroup->setExclusive(true);
243
244 QString defaultSpellcheckingLanguage = spellCheckingLanguage();
245 if (defaultSpellcheckingLanguage.isEmpty()) {
246 defaultSpellcheckingLanguage = d->speller->defaultLanguage();
247 }
248
249 QMapIterator<QString, QString> i(d->speller->availableDictionaries());
250 while (i.hasNext()) {
251 i.next();
252 QAction *languageAction = languagesMenu->addAction(i.key());
253 languageAction->setCheckable(true);
254 languageAction->setChecked(defaultSpellcheckingLanguage == i.value());
255 languageAction->setData(i.value());
256 languageAction->setActionGroup(languagesGroup);
257 connect(languageAction, &QAction::triggered, this, &RichTextEditor::slotLanguageSelected);
258 }
259 popup->addMenu(languagesMenu);
260 }
261 popup->addSeparator();
262 }
263 }
264
265 if (allowTabSupport() && !isReadOnly()) {
266 QAction *allowTabAction = popup->addAction(i18n("Allow Tabulations"));
267 allowTabAction->setCheckable(true);
268 allowTabAction->setChecked(!tabChangesFocus());
269 connect(allowTabAction, &QAction::triggered, this, &RichTextEditor::slotAllowTab);
270 }
271#if HAVE_KTEXTADDONS_TEXT_TO_SPEECH_SUPPORT
272 if (!emptyDocument) {
273 QAction *speakAction = popup->addAction(i18n("Speak Text"));
274 speakAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech")));
275 connect(speakAction, &QAction::triggered, this, &RichTextEditor::slotSpeakText);
276 }
277#endif
278#if HAVE_KTEXTADDONS_KIO_SUPPORT
279 if (webShortcutSupport() && textCursor().hasSelection()) {
280 popup->addSeparator();
281 const QString selectedText = textCursor().selectedText();
282 d->webshortcutMenuManager->setSelectedText(selectedText);
283 d->webshortcutMenuManager->addWebShortcutsToMenu(popup);
284 }
285#endif
286 if (emojiSupport()) {
287 popup->addSeparator();
288 auto action = new TextEmoticonsWidgets::EmoticonTextEditAction(this);
289 popup->addAction(action);
290 connect(action, &TextEmoticonsWidgets::EmoticonTextEditAction::insertEmoticon, this, &RichTextEditor::slotInsertEmoticon);
291 }
292 addExtraMenuEntry(popup, pos);
293 return popup;
294 }
295 return nullptr;
296}
297
298void RichTextEditor::slotInsertEmoticon(const QString &str)
299{
300 insertPlainText(str);
301}
302
303void RichTextEditor::slotSpeakText()
304{
305 QString text;
306 if (textCursor().hasSelection()) {
307 text = textCursor().selectedText();
308 } else {
309 text = toPlainText();
310 }
311 Q_EMIT say(text);
312}
313
314void RichTextEditor::setWebShortcutSupport(bool b)
315{
316#if HAVE_KTEXTADDONS_KIO_SUPPORT
317 if (b) {
318 d->supportFeatures |= AllowWebShortcut;
319 } else {
320 d->supportFeatures = (d->supportFeatures & ~AllowWebShortcut);
321 }
322#else
323 Q_UNUSED(b);
324#endif
325}
326
327bool RichTextEditor::webShortcutSupport() const
328{
329#if HAVE_KTEXTADDONS_KIO_SUPPORT
330 return d->supportFeatures & AllowWebShortcut;
331#else
332 return false;
333#endif
334}
335
336void RichTextEditor::setEmojiSupport(bool b)
337{
338 if (b) {
339 d->supportFeatures |= Emoji;
340 } else {
341 d->supportFeatures = (d->supportFeatures & ~Emoji);
342 }
343}
344
345bool RichTextEditor::emojiSupport() const
346{
347 return d->supportFeatures & Emoji;
348}
349
350void RichTextEditor::addIgnoreWords(const QStringList &lst)
351{
352 d->ignoreSpellCheckingWords = lst;
353 addIgnoreWordsToHighLighter();
354}
355
356void RichTextEditor::forceAutoCorrection(bool selectedText)
357{
358 Q_UNUSED(selectedText)
359 // Nothing here
360}
361
362void RichTextEditor::setSearchSupport(bool b)
363{
364 if (b) {
365 d->supportFeatures |= Search;
366 } else {
367 d->supportFeatures = (d->supportFeatures & ~Search);
368 }
369}
370
371bool RichTextEditor::searchSupport() const
372{
373 return d->supportFeatures & Search;
374}
375
376void RichTextEditor::setAllowTabSupport(bool b)
377{
378 if (b) {
379 d->supportFeatures |= AllowTab;
380 } else {
381 d->supportFeatures = (d->supportFeatures & ~AllowTab);
382 }
383}
384
385bool RichTextEditor::allowTabSupport() const
386{
387 return d->supportFeatures & AllowTab;
388}
389
390void RichTextEditor::setShowAutoCorrectButton(bool b)
391{
392 d->showAutoCorrectionButton = b;
393}
394
395bool RichTextEditor::showAutoCorrectButton() const
396{
397 return d->showAutoCorrectionButton;
398}
399
400bool RichTextEditor::spellCheckingSupport() const
401{
402 return d->supportFeatures & SpellChecking;
403}
404
405void RichTextEditor::setSpellCheckingSupport(bool check)
406{
407 if (check) {
408 d->supportFeatures |= SpellChecking;
409 } else {
410 d->supportFeatures = (d->supportFeatures & ~SpellChecking);
411 }
412}
413
414void RichTextEditor::setTextToSpeechSupport(bool b)
415{
416 if (b) {
417 d->supportFeatures |= TextToSpeech;
418 } else {
419 d->supportFeatures = (d->supportFeatures & ~TextToSpeech);
420 }
421}
422
423bool RichTextEditor::textToSpeechSupport() const
424{
425 return d->supportFeatures & TextToSpeech;
426}
427
428void RichTextEditor::slotAllowTab()
429{
431}
432
433void RichTextEditor::addExtraMenuEntry(QMenu *menu, QPoint pos)
434{
435 Q_UNUSED(menu)
436 Q_UNUSED(pos)
437}
438
439void RichTextEditor::slotUndoableClear()
440{
442 cursor.beginEditBlock();
443 cursor.movePosition(QTextCursor::Start);
445 cursor.removeSelectedText();
446 cursor.endEditBlock();
447}
448
449void RichTextEditor::updateReadOnlyColor()
450{
451 if (isReadOnly()) {
452 QPalette p = palette();
453 p.setColor(QPalette::Base, d->mReadOnlyBackgroundColor);
454 p.setColor(QPalette::Window, d->mReadOnlyBackgroundColor);
455 setPalette(p);
456 }
457}
458
459void RichTextEditor::setReadOnly(bool readOnly)
460{
461 if (!readOnly && hasFocus() && checkSpellingEnabled() && !d->richTextDecorator) {
462 createHighlighter();
463 }
464
465 if (readOnly == isReadOnly()) {
466 return;
467 }
468
469 if (readOnly) {
470 clearDecorator();
471 d->customPalette = testAttribute(Qt::WA_SetPalette);
472 updateReadOnlyColor();
473 } else {
474 if (d->customPalette && testAttribute(Qt::WA_SetPalette)) {
475 QPalette p = palette();
477 p.setColor(QPalette::Base, color);
478 p.setColor(QPalette::Window, color);
479 setPalette(p);
480 } else {
482 }
483 }
484
486}
487
488void RichTextEditor::checkSpelling(bool force)
489{
490 if (document()->isEmpty()) {
491 slotDisplayMessageIndicator(i18n("Nothing to spell check."));
492 if (force) {
493 Q_EMIT spellCheckingFinished();
494 }
495 return;
496 }
497 auto backgroundSpellCheck = new Sonnet::BackgroundChecker;
498 if (backgroundSpellCheck->speller().availableBackends().isEmpty()) {
499 if (force) {
500 const int answer = KMessageBox::questionTwoActions(this,
501 i18n("No backend available for spell checking. Do you want to send the email anyways?"),
502 QString(),
503 KGuiItem(i18nc("@action:button", "Send"), QStringLiteral("mail-send")),
505 if (answer == KMessageBox::ButtonCode::PrimaryAction) {
506 Q_EMIT spellCheckingFinished();
507 }
508 } else {
509 slotDisplayMessageIndicator(i18n("No backend available for spell checking."));
510 }
511 delete backgroundSpellCheck;
512 return;
513 }
514 if (!d->spellCheckingLanguage.isEmpty()) {
515 backgroundSpellCheck->changeLanguage(d->spellCheckingLanguage);
516 }
517 if (!d->ignoreSpellCheckingWords.isEmpty()) {
518 for (const QString &word : std::as_const(d->ignoreSpellCheckingWords)) {
519 backgroundSpellCheck->speller().addToSession(word);
520 }
521 }
522 auto spellDialog = new Sonnet::Dialog(backgroundSpellCheck, force ? this : nullptr);
523 auto buttonBox = spellDialog->findChild<QDialogButtonBox *>();
524 if (buttonBox) {
525 auto skipButton = new QPushButton(i18nc("@action:button", "Skip"));
526 buttonBox->addButton(skipButton, QDialogButtonBox::ActionRole);
527 connect(skipButton, &QPushButton::clicked, spellDialog, &Sonnet::Dialog::close);
528 if (force) {
529 connect(skipButton, &QPushButton::clicked, this, &RichTextEditor::spellCheckingFinished);
530 }
531 } else {
532 qCWarning(TEXTCUSTOMEDITOR_LOG) << " Impossible to find qdialogbuttonbox";
533 }
534 backgroundSpellCheck->setParent(spellDialog);
535 spellDialog->setAttribute(Qt::WA_DeleteOnClose, true);
536 spellDialog->activeAutoCorrect(d->showAutoCorrectionButton);
537 connect(spellDialog, &Sonnet::Dialog::replace, this, &RichTextEditor::slotSpellCheckerCorrected);
538 connect(spellDialog, &Sonnet::Dialog::misspelling, this, &RichTextEditor::slotSpellCheckerMisspelling);
539 connect(spellDialog, &Sonnet::Dialog::autoCorrect, this, &RichTextEditor::slotSpellCheckerAutoCorrect);
540 connect(spellDialog, &Sonnet::Dialog::spellCheckDone, this, &RichTextEditor::slotSpellCheckerFinished);
541 connect(spellDialog, &Sonnet::Dialog::cancel, this, &RichTextEditor::slotSpellCheckerCanceled);
542 connect(spellDialog, &Sonnet::Dialog::spellCheckStatus, this, &RichTextEditor::spellCheckStatus);
543 connect(spellDialog, &Sonnet::Dialog::languageChanged, this, &RichTextEditor::languageChanged);
544 if (force) {
545 connect(spellDialog, &Sonnet::Dialog::spellCheckDone, this, &RichTextEditor::spellCheckingFinished);
546 connect(spellDialog, &Sonnet::Dialog::cancel, this, &RichTextEditor::spellCheckingCanceled);
547 }
548 d->originalDoc = QTextDocumentFragment(document());
549 spellDialog->setBuffer(toPlainText());
550 spellDialog->show();
551}
552
553void RichTextEditor::slotCheckSpelling()
554{
555 checkSpelling(false);
556}
557
558void RichTextEditor::forceSpellChecking()
559{
560 checkSpelling(true);
561}
562
563void RichTextEditor::slotSpellCheckerCanceled()
564{
565 QTextDocument *doc = document();
566 doc->clear();
567 QTextCursor cursor(doc);
568 cursor.insertFragment(d->originalDoc);
569 slotSpellCheckerFinished();
570}
571
572void RichTextEditor::slotSpellCheckerAutoCorrect(const QString &currentWord, const QString &autoCorrectWord)
573{
574 Q_EMIT spellCheckerAutoCorrect(currentWord, autoCorrectWord);
575}
576
577void RichTextEditor::slotSpellCheckerMisspelling(const QString &text, int pos)
578{
579 highlightWord(text.length(), pos);
580}
581
582void RichTextEditor::slotSpellCheckerCorrected(const QString &oldWord, int pos, const QString &newWord)
583{
584 if (oldWord != newWord) {
586 cursor.setPosition(pos);
587 cursor.setPosition(pos + oldWord.length(), QTextCursor::KeepAnchor);
588 cursor.insertText(newWord);
589 }
590}
591
592void RichTextEditor::slotSpellCheckerFinished()
593{
595 cursor.clearSelection();
597 if (highlighter()) {
598 highlighter()->rehighlight();
599 }
600}
601
602void RichTextEditor::highlightWord(int length, int pos)
603{
605 cursor.setPosition(pos);
606 cursor.setPosition(pos + length, QTextCursor::KeepAnchor);
609}
610
611void RichTextEditor::createHighlighter()
612{
613 auto highlighter = new Sonnet::Highlighter(this);
614 highlighter->setCurrentLanguage(spellCheckingLanguage());
615 setHighlighter(highlighter);
616}
617
618Sonnet::SpellCheckDecorator *RichTextEditor::createSpellCheckDecorator()
619{
620 return new Sonnet::SpellCheckDecorator(this);
621}
622
623void RichTextEditor::addIgnoreWordsToHighLighter()
624{
625 if (d->ignoreSpellCheckingWords.isEmpty()) {
626 return;
627 }
628 if (d->richTextDecorator) {
629 Sonnet::Highlighter *_highlighter = d->richTextDecorator->highlighter();
630 for (const QString &word : std::as_const(d->ignoreSpellCheckingWords)) {
631 _highlighter->ignoreWord(word);
632 }
633 }
634}
635
636void RichTextEditor::setHighlighter(Sonnet::Highlighter *_highLighter)
637{
638 Sonnet::SpellCheckDecorator *decorator = createSpellCheckDecorator();
639 delete decorator->highlighter();
640 decorator->setHighlighter(_highLighter);
641
642 d->richTextDecorator = decorator;
643 addIgnoreWordsToHighLighter();
644}
645
646void RichTextEditor::focusInEvent(QFocusEvent *event)
647{
648 if (d->checkSpellingEnabled && !isReadOnly() && !d->richTextDecorator && spellCheckingSupport()) {
649 createHighlighter();
650 }
651
653}
654
655void RichTextEditor::setSpellCheckingConfigFileName(const QString &_fileName)
656{
657 d->spellCheckingConfigFileName = _fileName;
658 KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName);
659 if (config->hasGroup("Spelling"_L1)) {
660 KConfigGroup group(config, "Spelling"_L1);
661 d->checkSpellingEnabled = group.readEntry("checkerEnabledByDefault", false);
662 d->spellCheckingLanguage = group.readEntry("Language", QString());
663 }
664 setCheckSpellingEnabled(checkSpellingEnabled());
665
666 if (!d->spellCheckingLanguage.isEmpty() && highlighter()) {
667 highlighter()->setCurrentLanguage(d->spellCheckingLanguage);
668 highlighter()->rehighlight();
669 }
670}
671
672QString RichTextEditor::spellCheckingConfigFileName() const
673{
674 return d->spellCheckingConfigFileName;
675}
676
677bool RichTextEditor::checkSpellingEnabled() const
678{
679 return d->checkSpellingEnabled;
680}
681
682void RichTextEditor::setCheckSpellingEnabled(bool check)
683{
684 if (check == d->checkSpellingEnabled) {
685 return;
686 }
687 d->checkSpellingEnabled = check;
688 Q_EMIT checkSpellingChanged(check);
689 // From the above statement we know that if we're turning checking
690 // on that we need to create a new highlighter and if we're turning it
691 // off we should remove the old one.
692
693 if (check) {
694 if (hasFocus()) {
695 if (!d->richTextDecorator) {
696 createHighlighter();
697 }
698 if (!d->spellCheckingLanguage.isEmpty()) {
699 setSpellCheckingLanguage(spellCheckingLanguage());
700 }
701 }
702 } else {
703 clearDecorator();
704 }
705 updateHighLighter();
706}
707
708void RichTextEditor::updateHighLighter()
709{
710}
711
712void RichTextEditor::clearDecorator()
713{
714 delete d->richTextDecorator;
715 d->richTextDecorator = nullptr;
716}
717
718const QString &RichTextEditor::spellCheckingLanguage() const
719{
720 return d->spellCheckingLanguage;
721}
722
723void RichTextEditor::setSpellCheckingLanguage(const QString &_language)
724{
725 if (highlighter()) {
726 highlighter()->setCurrentLanguage(_language);
727 }
728
729 if (_language != d->spellCheckingLanguage) {
730 d->spellCheckingLanguage = _language;
731 KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName);
732 KConfigGroup group(config, "Spelling"_L1);
733 group.writeEntry("Language", d->spellCheckingLanguage);
734
735 Q_EMIT languageChanged(_language);
736 }
737}
738
739void RichTextEditor::slotToggleAutoSpellCheck()
740{
741 setCheckSpellingEnabled(!checkSpellingEnabled());
742 KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName);
743 KConfigGroup group(config, "Spelling"_L1);
744 group.writeEntry("checkerEnabledByDefault", d->checkSpellingEnabled);
745}
746
747void RichTextEditor::slotLanguageSelected()
748{
749 auto languageAction = static_cast<QAction *>(QObject::sender());
750 setSpellCheckingLanguage(languageAction->data().toString());
751}
752
753static void deleteWord(QTextCursor cursor, QTextCursor::MoveOperation op)
754{
755 cursor.clearSelection();
757 cursor.removeSelectedText();
758}
759
760void RichTextEditor::deleteWordBack()
761{
763}
764
765void RichTextEditor::deleteWordForward()
766{
767 deleteWord(textCursor(), QTextCursor::WordRight);
768}
769
770bool RichTextEditor::event(QEvent *ev)
771{
772 if (ev->type() == QEvent::ShortcutOverride) {
773 auto e = static_cast<QKeyEvent *>(ev);
774 if (overrideShortcut(e)) {
775 e->accept();
776 return true;
777 }
778 } else if (ev->type() == QEvent::ApplicationPaletteChange) {
779 regenerateColorScheme();
780 }
781 return QTextEdit::event(ev);
782}
783
784void RichTextEditor::wheelEvent(QWheelEvent *event)
785{
787 const int angleDeltaY{event->angleDelta().y()};
788 if (angleDeltaY > 0) {
789 zoomIn();
790 } else if (angleDeltaY < 0) {
791 zoomOut();
792 }
793 event->accept();
794 return;
795 }
797}
798
799bool RichTextEditor::handleShortcut(QKeyEvent *event)
800{
801 const int key = event->key() | event->modifiers();
802
803 if (KStandardShortcut::copy().contains(key)) {
804 copy();
805 return true;
806 } else if (KStandardShortcut::paste().contains(key)) {
807 paste();
808 return true;
809 } else if (KStandardShortcut::cut().contains(key)) {
810 cut();
811 return true;
812 } else if (KStandardShortcut::undo().contains(key)) {
813 if (!isReadOnly()) {
814 undo();
815 }
816 return true;
817 } else if (KStandardShortcut::redo().contains(key)) {
818 if (!isReadOnly()) {
819 redo();
820 }
821 return true;
822 } else if (KStandardShortcut::deleteWordBack().contains(key)) {
823 if (!isReadOnly()) {
824 deleteWordBack();
825 }
826 return true;
827 } else if (KStandardShortcut::deleteWordForward().contains(key)) {
828 if (!isReadOnly()) {
829 deleteWordForward();
830 }
831 return true;
832 } else if (KStandardShortcut::backwardWord().contains(key)) {
834 cursor.movePosition(QTextCursor::PreviousWord);
836 return true;
837 } else if (KStandardShortcut::forwardWord().contains(key)) {
839 cursor.movePosition(QTextCursor::NextWord);
841 return true;
842 } else if (KStandardShortcut::next().contains(key)) {
844 bool moved = false;
845 qreal lastY = cursorRect(cursor).bottom();
846 qreal distance = 0;
847 do {
848 qreal y = cursorRect(cursor).bottom();
849 distance += qAbs(y - lastY);
850 lastY = y;
851 moved = cursor.movePosition(QTextCursor::Down);
852 } while (moved && distance < viewport()->height());
853
854 if (moved) {
855 cursor.movePosition(QTextCursor::Up);
857 }
859 return true;
860 } else if (KStandardShortcut::prior().contains(key)) {
862 bool moved = false;
863 qreal lastY = cursorRect(cursor).bottom();
864 qreal distance = 0;
865 do {
866 qreal y = cursorRect(cursor).bottom();
867 distance += qAbs(y - lastY);
868 lastY = y;
869 moved = cursor.movePosition(QTextCursor::Up);
870 } while (moved && distance < viewport()->height());
871
872 if (moved) {
873 cursor.movePosition(QTextCursor::Down);
875 }
877 return true;
878 } else if (KStandardShortcut::begin().contains(key)) {
880 cursor.movePosition(QTextCursor::Start);
882 return true;
883 } else if (KStandardShortcut::end().contains(key)) {
885 cursor.movePosition(QTextCursor::End);
887 return true;
888 } else if (KStandardShortcut::beginningOfLine().contains(key)) {
890 cursor.movePosition(QTextCursor::StartOfLine);
892 return true;
893 } else if (KStandardShortcut::endOfLine().contains(key)) {
895 cursor.movePosition(QTextCursor::EndOfLine);
897 return true;
898 } else if (searchSupport() && KStandardShortcut::find().contains(key)) {
899 Q_EMIT findText();
900 return true;
901 } else if (searchSupport() && KStandardShortcut::replace().contains(key)) {
902 if (!isReadOnly()) {
903 Q_EMIT replaceText();
904 }
905 return true;
906 } else if (KStandardShortcut::pasteSelection().contains(key)) {
908 if (!text.isEmpty()) {
909 insertPlainText(text); // TODO: check if this is html? (MiB)
910 }
911 return true;
912 } else if (event == QKeySequence::DeleteEndOfLine) {
914 QTextBlock block = cursor.block();
915 if (cursor.position() == block.position() + block.length() - 2) {
917 } else {
919 }
920 cursor.removeSelectedText();
922 return true;
923 }
924
925 return false;
926}
927
928bool RichTextEditor::overrideShortcut(QKeyEvent *event)
929{
930 const int key = event->key() | event->modifiers();
931
932 if (KStandardShortcut::copy().contains(key)) {
933 return true;
934 } else if (KStandardShortcut::paste().contains(key)) {
935 return true;
936 } else if (KStandardShortcut::cut().contains(key)) {
937 return true;
938 } else if (KStandardShortcut::undo().contains(key)) {
939 return true;
940 } else if (KStandardShortcut::redo().contains(key)) {
941 return true;
942 } else if (KStandardShortcut::deleteWordBack().contains(key)) {
943 return true;
944 } else if (KStandardShortcut::deleteWordForward().contains(key)) {
945 return true;
946 } else if (KStandardShortcut::backwardWord().contains(key)) {
947 return true;
948 } else if (KStandardShortcut::forwardWord().contains(key)) {
949 return true;
950 } else if (KStandardShortcut::next().contains(key)) {
951 return true;
952 } else if (KStandardShortcut::prior().contains(key)) {
953 return true;
954 } else if (KStandardShortcut::begin().contains(key)) {
955 return true;
956 } else if (KStandardShortcut::end().contains(key)) {
957 return true;
958 } else if (KStandardShortcut::beginningOfLine().contains(key)) {
959 return true;
960 } else if (KStandardShortcut::endOfLine().contains(key)) {
961 return true;
962 } else if (KStandardShortcut::pasteSelection().contains(key)) {
963 return true;
964 } else if (searchSupport() && KStandardShortcut::find().contains(key)) {
965 return true;
966 } else if (searchSupport() && KStandardShortcut::findNext().contains(key)) {
967 return true;
968 } else if (searchSupport() && KStandardShortcut::replace().contains(key)) {
969 return true;
970 } else if (event->matches(QKeySequence::SelectAll)) { // currently missing in QTextEdit
971 return true;
972 } else if (event == QKeySequence::DeleteEndOfLine) {
973 return true;
974 }
975 return false;
976}
977
978void RichTextEditor::keyPressEvent(QKeyEvent *event)
979{
980 const bool isControlClicked = event->modifiers() & Qt::ControlModifier;
981 const bool isShiftClicked = event->modifiers() & Qt::ShiftModifier;
982 if (handleShortcut(event)) {
983 event->accept();
984 } else if (event->key() == Qt::Key_Up && isControlClicked && isShiftClicked) {
985 moveLineUpDown(true);
986 event->accept();
987 } else if (event->key() == Qt::Key_Down && isControlClicked && isShiftClicked) {
988 moveLineUpDown(false);
989 event->accept();
990 } else if (event->key() == Qt::Key_Up && isControlClicked) {
991 moveCursorBeginUpDown(true);
992 event->accept();
993 } else if (event->key() == Qt::Key_Down && isControlClicked) {
994 moveCursorBeginUpDown(false);
995 event->accept();
996 } else {
998 }
999}
1000
1001int RichTextEditor::zoomFactor() const
1002{
1003 int pourcentage = 100;
1004 const QFont f = font();
1005 if (d->mInitialFontSize != f.pointSize()) {
1006 pourcentage = (f.pointSize() * 100) / d->mInitialFontSize;
1007 }
1008 return pourcentage;
1009}
1010
1011void RichTextEditor::slotZoomReset()
1012{
1013 QFont f = font();
1014 if (d->mInitialFontSize != f.pointSize()) {
1015 f.setPointSize(d->mInitialFontSize);
1016 setFont(f);
1017 }
1018}
1019
1020void RichTextEditor::moveCursorBeginUpDown(bool moveUp)
1021{
1025 cursor.clearSelection();
1026 move.movePosition(QTextCursor::StartOfBlock);
1028 move.endEditBlock();
1030}
1031
1032void RichTextEditor::moveLineUpDown(bool moveUp)
1033{
1037
1038 const bool hasSelection = cursor.hasSelection();
1039
1040 if (hasSelection) {
1041 move.setPosition(cursor.selectionStart());
1042 move.movePosition(QTextCursor::StartOfBlock);
1043 move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
1045 } else {
1046 move.movePosition(QTextCursor::StartOfBlock);
1048 }
1049 const QString text = move.selectedText();
1050
1052 move.removeSelectedText();
1053
1054 if (moveUp) {
1055 move.movePosition(QTextCursor::PreviousBlock);
1056 move.insertBlock();
1057 move.movePosition(QTextCursor::Left);
1058 } else {
1059 move.movePosition(QTextCursor::EndOfBlock);
1060 if (move.atBlockStart()) { // empty block
1061 move.movePosition(QTextCursor::NextBlock);
1062 move.insertBlock();
1063 move.movePosition(QTextCursor::Left);
1064 } else {
1065 move.insertBlock();
1066 }
1067 }
1068
1069 int start = move.position();
1070 move.clearSelection();
1071 move.insertText(text);
1072 int end = move.position();
1073
1074 if (hasSelection) {
1075 move.setPosition(end);
1076 move.setPosition(start, QTextCursor::KeepAnchor);
1077 } else {
1078 move.setPosition(start);
1079 }
1080 move.endEditBlock();
1081
1083}
1084
1085#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)
ButtonCode questionTwoActions(QWidget *parent, const QString &text, const QString &title, const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const QString &dontAskAgainName=QString(), Options options=Notify)
QAction * replace(const QObject *recvr, const char *slot, QObject *parent)
QAction * find(const QObject *recvr, const char *slot, QObject *parent)
QAction * clear(const QObject *recvr, const char *slot, QObject *parent)
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)
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)
virtual bool event(QEvent *e)
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 Aug 30 2024 11:47:25 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.