1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2002 Carsten Pfeiffer <pfeiffer@kde.org>
4 SPDX-FileCopyrightText: 2005 Michael Brade <brade@kde.org>
5 SPDX-FileCopyrightText: 2012 Laurent Montel <montel@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "ktextedit.h"
11#include "ktextedit_p.h"
12
13#include <QAction>
14#include <QActionGroup>
15#include <QApplication>
16#include <QClipboard>
17#include <QDebug>
18#include <QKeyEvent>
19#include <QMenu>
20#include <QScrollBar>
21#include <QTextCursor>
22
23#include <KCursor>
24#include <KLocalizedString>
25#include <KMessageBox>
26#include <KStandardActions>
27#include <KStandardShortcut>
28#include <sonnet/backgroundchecker.h>
29#include <sonnet/configdialog.h>
30#include <sonnet/dialog.h>
31
32class KTextDecorator : public Sonnet::SpellCheckDecorator
33{
34public:
35 explicit KTextDecorator(KTextEdit *textEdit);
36 bool isSpellCheckingEnabledForBlock(const QString &textBlock) const override;
37
38private:
39 KTextEdit *m_textEdit;
40};
41
42void KTextEditPrivate::checkSpelling(bool force)
43{
44 Q_Q(KTextEdit);
45
46 if (q->document()->isEmpty()) {
47 KMessageBox::information(parent: q, i18n("Nothing to spell check."));
48 if (force) {
49 Q_EMIT q->spellCheckingFinished();
50 }
51 return;
52 }
53 Sonnet::BackgroundChecker *backgroundSpellCheck = new Sonnet::BackgroundChecker;
54 if (!spellCheckingLanguage.isEmpty()) {
55 backgroundSpellCheck->changeLanguage(lang: spellCheckingLanguage);
56 }
57 Sonnet::Dialog *spellDialog = new Sonnet::Dialog(backgroundSpellCheck, force ? q : nullptr);
58 backgroundSpellCheck->setParent(spellDialog);
59 spellDialog->setAttribute(Qt::WA_DeleteOnClose, on: true);
60 spellDialog->activeAutoCorrect(active: showAutoCorrectionButton);
61 QObject::connect(sender: spellDialog, signal: &Sonnet::Dialog::replace, context: q, slot: [this](const QString &oldWord, int pos, const QString &newWord) {
62 spellCheckerCorrected(oldWord, pos, newWord);
63 });
64 QObject::connect(sender: spellDialog, signal: &Sonnet::Dialog::misspelling, context: q, slot: [this](const QString &text, int pos) {
65 spellCheckerMisspelling(text, pos);
66 });
67 QObject::connect(sender: spellDialog, signal: &Sonnet::Dialog::autoCorrect, context: q, slot: &KTextEdit::spellCheckerAutoCorrect);
68 QObject::connect(sender: spellDialog, signal: &Sonnet::Dialog::spellCheckDone, context: q, slot: [this]() {
69 spellCheckerFinished();
70 });
71 QObject::connect(sender: spellDialog, signal: &Sonnet::Dialog::cancel, context: q, slot: [this]() {
72 spellCheckerCanceled();
73 });
74
75 // Laurent in sonnet/dialog.cpp we emit done(QString) too => it calls here twice spellCheckerFinished not necessary
76 // connect(spellDialog, SIGNAL(stop()), q, SLOT(spellCheckerFinished()));
77
78 QObject::connect(sender: spellDialog, signal: &Sonnet::Dialog::spellCheckStatus, context: q, slot: &KTextEdit::spellCheckStatus);
79 QObject::connect(sender: spellDialog, signal: &Sonnet::Dialog::languageChanged, context: q, slot: &KTextEdit::languageChanged);
80 if (force) {
81 QObject::connect(sender: spellDialog, signal: &Sonnet::Dialog::spellCheckDone, context: q, slot: &KTextEdit::spellCheckingFinished);
82 QObject::connect(sender: spellDialog, signal: &Sonnet::Dialog::cancel, context: q, slot: &KTextEdit::spellCheckingCanceled);
83 // Laurent in sonnet/dialog.cpp we emit done(QString) too => it calls here twice spellCheckerFinished not necessary
84 // connect(spellDialog, SIGNAL(stop()), q, SIGNAL(spellCheckingFinished()));
85 }
86 originalDoc = QTextDocumentFragment(q->document());
87 spellDialog->setBuffer(q->toPlainText());
88 spellDialog->show();
89}
90
91void KTextEditPrivate::spellCheckerCanceled()
92{
93 Q_Q(KTextEdit);
94
95 QTextDocument *doc = q->document();
96 doc->clear();
97 QTextCursor cursor(doc);
98 cursor.insertFragment(fragment: originalDoc);
99 spellCheckerFinished();
100}
101
102void KTextEditPrivate::spellCheckerAutoCorrect(const QString &currentWord, const QString &autoCorrectWord)
103{
104 Q_Q(KTextEdit);
105
106 Q_EMIT q->spellCheckerAutoCorrect(currentWord, autoCorrectWord);
107}
108
109void KTextEditPrivate::spellCheckerMisspelling(const QString &text, int pos)
110{
111 Q_Q(KTextEdit);
112
113 // qDebug()<<"TextEdit::Private::spellCheckerMisspelling :"<<text<<" pos :"<<pos;
114 q->highlightWord(length: text.length(), pos);
115}
116
117void KTextEditPrivate::spellCheckerCorrected(const QString &oldWord, int pos, const QString &newWord)
118{
119 Q_Q(KTextEdit);
120
121 // qDebug()<<" oldWord :"<<oldWord<<" newWord :"<<newWord<<" pos : "<<pos;
122 if (oldWord != newWord) {
123 QTextCursor cursor(q->document());
124 cursor.setPosition(pos);
125 cursor.setPosition(pos: pos + oldWord.length(), mode: QTextCursor::KeepAnchor);
126 cursor.insertText(text: newWord);
127 }
128}
129
130void KTextEditPrivate::spellCheckerFinished()
131{
132 Q_Q(KTextEdit);
133
134 QTextCursor cursor(q->document());
135 cursor.clearSelection();
136 q->setTextCursor(cursor);
137 if (q->highlighter()) {
138 q->highlighter()->rehighlight();
139 }
140}
141
142void KTextEditPrivate::toggleAutoSpellCheck()
143{
144 Q_Q(KTextEdit);
145
146 q->setCheckSpellingEnabled(!q->checkSpellingEnabled());
147}
148
149void KTextEditPrivate::undoableClear()
150{
151 Q_Q(KTextEdit);
152
153 QTextCursor cursor = q->textCursor();
154 cursor.beginEditBlock();
155 cursor.movePosition(op: QTextCursor::Start);
156 cursor.movePosition(op: QTextCursor::End, QTextCursor::KeepAnchor);
157 cursor.removeSelectedText();
158 cursor.endEditBlock();
159}
160
161void KTextEditPrivate::slotAllowTab()
162{
163 Q_Q(KTextEdit);
164
165 q->setTabChangesFocus(!q->tabChangesFocus());
166}
167
168void KTextEditPrivate::menuActivated(QAction *action)
169{
170 Q_Q(KTextEdit);
171
172 if (action == spellCheckAction) {
173 q->checkSpelling();
174 } else if (action == autoSpellCheckAction) {
175 toggleAutoSpellCheck();
176 } else if (action == allowTab) {
177 slotAllowTab();
178 }
179}
180
181void KTextEditPrivate::slotFindHighlight(const QString &text, int matchingIndex, int matchingLength)
182{
183 Q_Q(KTextEdit);
184
185 Q_UNUSED(text)
186 // qDebug() << "Highlight: [" << text << "] mi:" << matchingIndex << " ml:" << matchingLength;
187 QTextCursor tc = q->textCursor();
188 tc.setPosition(pos: matchingIndex);
189 tc.movePosition(op: QTextCursor::NextCharacter, QTextCursor::KeepAnchor, n: matchingLength);
190 q->setTextCursor(tc);
191 q->ensureCursorVisible();
192}
193
194void KTextEditPrivate::slotReplaceText(const QString &text, int replacementIndex, int replacedLength, int matchedLength)
195{
196 Q_Q(KTextEdit);
197
198 // qDebug() << "Replace: [" << text << "] ri:" << replacementIndex << " rl:" << replacedLength << " ml:" << matchedLength;
199 QTextCursor tc = q->textCursor();
200 tc.setPosition(pos: replacementIndex);
201 tc.movePosition(op: QTextCursor::NextCharacter, QTextCursor::KeepAnchor, n: matchedLength);
202 tc.removeSelectedText();
203 tc.insertText(text: text.mid(position: replacementIndex, n: replacedLength));
204 if (replace->options() & KReplaceDialog::PromptOnReplace) {
205 q->setTextCursor(tc);
206 q->ensureCursorVisible();
207 }
208 lastReplacedPosition = replacementIndex;
209}
210
211void KTextEditPrivate::init()
212{
213 Q_Q(KTextEdit);
214
215 KCursor::setAutoHideCursor(w: q, enable: true, customEventFilter: false);
216 q->connect(sender: q, signal: &KTextEdit::languageChanged, context: q, slot: &KTextEdit::setSpellCheckingLanguage);
217}
218
219KTextDecorator::KTextDecorator(KTextEdit *textEdit)
220 : SpellCheckDecorator(textEdit)
221 , m_textEdit(textEdit)
222{
223}
224
225bool KTextDecorator::isSpellCheckingEnabledForBlock(const QString &textBlock) const
226{
227 return m_textEdit->shouldBlockBeSpellChecked(block: textBlock);
228}
229
230KTextEdit::KTextEdit(const QString &text, QWidget *parent)
231 : KTextEdit(*new KTextEditPrivate(this), text, parent)
232{
233}
234
235KTextEdit::KTextEdit(KTextEditPrivate &dd, const QString &text, QWidget *parent)
236 : QTextEdit(text, parent)
237 , d_ptr(&dd)
238{
239 Q_D(KTextEdit);
240
241 d->init();
242}
243
244KTextEdit::KTextEdit(QWidget *parent)
245 : KTextEdit(*new KTextEditPrivate(this), parent)
246{
247}
248
249KTextEdit::KTextEdit(KTextEditPrivate &dd, QWidget *parent)
250 : QTextEdit(parent)
251 , d_ptr(&dd)
252{
253 Q_D(KTextEdit);
254
255 d->init();
256}
257
258KTextEdit::~KTextEdit() = default;
259
260const QString &KTextEdit::spellCheckingLanguage() const
261{
262 Q_D(const KTextEdit);
263
264 return d->spellCheckingLanguage;
265}
266
267void KTextEdit::setSpellCheckingLanguage(const QString &_language)
268{
269 Q_D(KTextEdit);
270
271 if (highlighter()) {
272 highlighter()->setCurrentLanguage(_language);
273 highlighter()->rehighlight();
274 }
275
276 if (_language != d->spellCheckingLanguage) {
277 d->spellCheckingLanguage = _language;
278 Q_EMIT languageChanged(language: _language);
279 }
280}
281
282void KTextEdit::showSpellConfigDialog(const QString &windowIcon)
283{
284 Q_D(KTextEdit);
285
286 Sonnet::ConfigDialog dialog(this);
287 if (!d->spellCheckingLanguage.isEmpty()) {
288 dialog.setLanguage(d->spellCheckingLanguage);
289 }
290 if (!windowIcon.isEmpty()) {
291 dialog.setWindowIcon(QIcon::fromTheme(name: windowIcon, fallback: dialog.windowIcon()));
292 }
293 if (dialog.exec()) {
294 setSpellCheckingLanguage(dialog.language());
295 }
296}
297
298bool KTextEdit::event(QEvent *ev)
299{
300 Q_D(KTextEdit);
301
302 if (ev->type() == QEvent::ShortcutOverride) {
303 QKeyEvent *e = static_cast<QKeyEvent *>(ev);
304 if (d->overrideShortcut(e)) {
305 e->accept();
306 return true;
307 }
308 }
309 return QTextEdit::event(e: ev);
310}
311
312bool KTextEditPrivate::handleShortcut(const QKeyEvent *event)
313{
314 Q_Q(KTextEdit);
315
316 const int key = event->key() | event->modifiers();
317
318 if (KStandardShortcut::copy().contains(t: key)) {
319 q->copy();
320 return true;
321 } else if (KStandardShortcut::paste().contains(t: key)) {
322 q->paste();
323 return true;
324 } else if (KStandardShortcut::cut().contains(t: key)) {
325 q->cut();
326 return true;
327 } else if (KStandardShortcut::undo().contains(t: key)) {
328 if (!q->isReadOnly()) {
329 q->undo();
330 }
331 return true;
332 } else if (KStandardShortcut::redo().contains(t: key)) {
333 if (!q->isReadOnly()) {
334 q->redo();
335 }
336 return true;
337 } else if (KStandardShortcut::deleteWordBack().contains(t: key)) {
338 if (!q->isReadOnly()) {
339 q->deleteWordBack();
340 }
341 return true;
342 } else if (KStandardShortcut::deleteWordForward().contains(t: key)) {
343 if (!q->isReadOnly()) {
344 q->deleteWordForward();
345 }
346 return true;
347 } else if (KStandardShortcut::backwardWord().contains(t: key)) {
348 QTextCursor cursor = q->textCursor();
349 // We use visual positioning here since keyboard arrows represents visual direction (left, right)
350 cursor.movePosition(op: QTextCursor::WordLeft);
351 q->setTextCursor(cursor);
352 return true;
353 } else if (KStandardShortcut::forwardWord().contains(t: key)) {
354 QTextCursor cursor = q->textCursor();
355 // We use visual positioning here since keyboard arrows represents visual direction (left, right)
356 cursor.movePosition(op: QTextCursor::WordRight);
357 q->setTextCursor(cursor);
358 return true;
359 } else if (KStandardShortcut::next().contains(t: key)) {
360 QTextCursor cursor = q->textCursor();
361 bool moved = false;
362 qreal lastY = q->cursorRect(cursor).bottom();
363 qreal distance = 0;
364 do {
365 qreal y = q->cursorRect(cursor).bottom();
366 distance += qAbs(t: y - lastY);
367 lastY = y;
368 moved = cursor.movePosition(op: QTextCursor::Down);
369 } while (moved && distance < q->viewport()->height());
370
371 if (moved) {
372 cursor.movePosition(op: QTextCursor::Up);
373 q->verticalScrollBar()->triggerAction(action: QAbstractSlider::SliderPageStepAdd);
374 }
375 q->setTextCursor(cursor);
376 return true;
377 } else if (KStandardShortcut::prior().contains(t: key)) {
378 QTextCursor cursor = q->textCursor();
379 bool moved = false;
380 qreal lastY = q->cursorRect(cursor).bottom();
381 qreal distance = 0;
382 do {
383 qreal y = q->cursorRect(cursor).bottom();
384 distance += qAbs(t: y - lastY);
385 lastY = y;
386 moved = cursor.movePosition(op: QTextCursor::Up);
387 } while (moved && distance < q->viewport()->height());
388
389 if (moved) {
390 cursor.movePosition(op: QTextCursor::Down);
391 q->verticalScrollBar()->triggerAction(action: QAbstractSlider::SliderPageStepSub);
392 }
393 q->setTextCursor(cursor);
394 return true;
395 } else if (KStandardShortcut::begin().contains(t: key)) {
396 QTextCursor cursor = q->textCursor();
397 cursor.movePosition(op: QTextCursor::Start);
398 q->setTextCursor(cursor);
399 return true;
400 } else if (KStandardShortcut::end().contains(t: key)) {
401 QTextCursor cursor = q->textCursor();
402 cursor.movePosition(op: QTextCursor::End);
403 q->setTextCursor(cursor);
404 return true;
405 } else if (KStandardShortcut::beginningOfLine().contains(t: key)) {
406 QTextCursor cursor = q->textCursor();
407 cursor.movePosition(op: QTextCursor::StartOfLine);
408 q->setTextCursor(cursor);
409 return true;
410 } else if (KStandardShortcut::endOfLine().contains(t: key)) {
411 QTextCursor cursor = q->textCursor();
412 cursor.movePosition(op: QTextCursor::EndOfLine);
413 q->setTextCursor(cursor);
414 return true;
415 } else if (findReplaceEnabled && KStandardShortcut::find().contains(t: key)) {
416 q->slotFind();
417 return true;
418 } else if (findReplaceEnabled && KStandardShortcut::findNext().contains(t: key)) {
419 q->slotFindNext();
420 return true;
421 } else if (findReplaceEnabled && KStandardShortcut::findPrev().contains(t: key)) {
422 q->slotFindPrevious();
423 return true;
424 } else if (findReplaceEnabled && KStandardShortcut::replace().contains(t: key)) {
425 if (!q->isReadOnly()) {
426 q->slotReplace();
427 }
428 return true;
429 } else if (KStandardShortcut::pasteSelection().contains(t: key)) {
430 QString text = QApplication::clipboard()->text(mode: QClipboard::Selection);
431 if (!text.isEmpty()) {
432 q->insertPlainText(text); // TODO: check if this is html? (MiB)
433 }
434 return true;
435 }
436 return false;
437}
438
439static void deleteWord(QTextCursor cursor, QTextCursor::MoveOperation op)
440{
441 cursor.clearSelection();
442 cursor.movePosition(op, QTextCursor::KeepAnchor);
443 cursor.removeSelectedText();
444}
445
446void KTextEdit::deleteWordBack()
447{
448 // We use logical positioning here since deleting should always delete the previous word
449 // (left in case of LTR text, right in case of RTL text)
450 deleteWord(cursor: textCursor(), op: QTextCursor::PreviousWord);
451}
452
453void KTextEdit::deleteWordForward()
454{
455 // We use logical positioning here since deleting should always delete the previous word
456 // (left in case of LTR text, right in case of RTL text)
457 deleteWord(cursor: textCursor(), op: QTextCursor::NextWord);
458}
459
460QMenu *KTextEdit::mousePopupMenu()
461{
462 Q_D(KTextEdit);
463
464 QMenu *popup = createStandardContextMenu();
465 if (!popup) {
466 return nullptr;
467 }
468 connect(sender: popup, signal: &QMenu::triggered, context: this, slot: [d](QAction *action) {
469 d->menuActivated(action);
470 });
471
472 const bool emptyDocument = document()->isEmpty();
473 if (!isReadOnly()) {
474 QList<QAction *> actionList = popup->actions();
475 enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs };
476 QAction *separatorAction = nullptr;
477 int idx = actionList.indexOf(t: actionList[SelectAllAct]) + 1;
478 if (idx < actionList.count()) {
479 separatorAction = actionList.at(i: idx);
480 }
481
482 auto undoableClearSlot = [d]() {
483 d->undoableClear();
484 };
485
486 if (separatorAction) {
487 QAction *clearAllAction = KStandardActions::clear(recvr: this, slot: undoableClearSlot, parent: popup);
488 if (emptyDocument) {
489 clearAllAction->setEnabled(false);
490 }
491 popup->insertAction(before: separatorAction, action: clearAllAction);
492 }
493 }
494
495 if (!isReadOnly()) {
496 popup->addSeparator();
497 if (!d->speller) {
498 d->speller = new Sonnet::Speller();
499 }
500 if (!d->speller->availableBackends().isEmpty()) {
501 d->spellCheckAction = popup->addAction(icon: QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18nc("@action:inmenu", "Check Spelling…"));
502 if (emptyDocument) {
503 d->spellCheckAction->setEnabled(false);
504 }
505 if (checkSpellingEnabled()) {
506 d->languagesMenu = new QMenu(i18n("Spell Checking Language"), popup);
507 QActionGroup *languagesGroup = new QActionGroup(d->languagesMenu);
508 languagesGroup->setExclusive(true);
509
510 QMapIterator<QString, QString> i(d->speller->availableDictionaries());
511 const QString language = spellCheckingLanguage();
512 while (i.hasNext()) {
513 i.next();
514
515 QAction *languageAction = d->languagesMenu->addAction(text: i.key());
516 languageAction->setCheckable(true);
517 languageAction->setChecked(language == i.value() || (language.isEmpty() && d->speller->defaultLanguage() == i.value()));
518 languageAction->setData(i.value());
519 languageAction->setActionGroup(languagesGroup);
520 connect(sender: languageAction, signal: &QAction::triggered, slot: [this, languageAction]() {
521 setSpellCheckingLanguage(languageAction->data().toString());
522 });
523 }
524 popup->addMenu(menu: d->languagesMenu);
525 }
526
527 d->autoSpellCheckAction = popup->addAction(i18n("Auto Spell Check"));
528 d->autoSpellCheckAction->setCheckable(true);
529 d->autoSpellCheckAction->setChecked(checkSpellingEnabled());
530 popup->addSeparator();
531 }
532 if (d->showTabAction) {
533 d->allowTab = popup->addAction(i18n("Allow Tabulations"));
534 d->allowTab->setCheckable(true);
535 d->allowTab->setChecked(!tabChangesFocus());
536 }
537 }
538
539 if (d->findReplaceEnabled) {
540 QAction *findAction = KStandardActions::find(recvr: this, slot: &KTextEdit::slotFind, parent: popup);
541 QAction *findNextAction = KStandardActions::findNext(recvr: this, slot: &KTextEdit::slotFindNext, parent: popup);
542 QAction *findPrevAction = KStandardActions::findPrev(recvr: this, slot: &KTextEdit::slotFindPrevious, parent: popup);
543 if (emptyDocument) {
544 findAction->setEnabled(false);
545 findNextAction->setEnabled(false);
546 findPrevAction->setEnabled(false);
547 } else {
548 findNextAction->setEnabled(d->find != nullptr);
549 findPrevAction->setEnabled(d->find != nullptr);
550 }
551 popup->addSeparator();
552 popup->addAction(action: findAction);
553 popup->addAction(action: findNextAction);
554 popup->addAction(action: findPrevAction);
555
556 if (!isReadOnly()) {
557 QAction *replaceAction = KStandardActions::replace(recvr: this, slot: &KTextEdit::slotReplace, parent: popup);
558 if (emptyDocument) {
559 replaceAction->setEnabled(false);
560 }
561 popup->addAction(action: replaceAction);
562 }
563 }
564#ifdef HAVE_SPEECH
565 popup->addSeparator();
566 QAction *speakAction = popup->addAction(i18n("Speak Text"));
567 speakAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech")));
568 speakAction->setEnabled(!emptyDocument);
569 connect(sender: speakAction, signal: &QAction::triggered, context: this, slot: &KTextEdit::slotSpeakText);
570#endif
571 return popup;
572}
573
574void KTextEdit::slotSpeakText()
575{
576#ifdef HAVE_SPEECH
577 Q_D(KTextEdit);
578 QString text;
579 if (textCursor().hasSelection()) {
580 text = textCursor().selectedText();
581 } else {
582 text = toPlainText();
583 }
584 if (!d->textToSpeech) {
585 d->textToSpeech = new QTextToSpeech(this);
586 }
587 d->textToSpeech->say(text);
588#endif
589}
590
591void KTextEdit::contextMenuEvent(QContextMenuEvent *event)
592{
593 QMenu *popup = mousePopupMenu();
594 if (popup) {
595 aboutToShowContextMenu(menu: popup);
596 popup->exec(pos: event->globalPos());
597 delete popup;
598 }
599}
600
601void KTextEdit::createHighlighter()
602{
603 setHighlighter(new Sonnet::Highlighter(this));
604}
605
606Sonnet::Highlighter *KTextEdit::highlighter() const
607{
608 Q_D(const KTextEdit);
609
610 if (d->decorator) {
611 return d->decorator->highlighter();
612 } else {
613 return nullptr;
614 }
615}
616
617void KTextEdit::clearDecorator()
618{
619 Q_D(KTextEdit);
620
621 // Set pointer to null before deleting KTextDecorator as dtor will emit signal,
622 // which could call this code again and cause double delete/crash
623 auto decorator = d->decorator;
624 d->decorator = nullptr;
625 delete decorator;
626}
627
628void KTextEdit::addTextDecorator(Sonnet::SpellCheckDecorator *decorator)
629{
630 Q_D(KTextEdit);
631
632 d->decorator = decorator;
633}
634
635void KTextEdit::setHighlighter(Sonnet::Highlighter *_highLighter)
636{
637 KTextDecorator *decorator = new KTextDecorator(this);
638 // The old default highlighter must be manually deleted.
639 delete decorator->highlighter();
640 decorator->setHighlighter(_highLighter);
641
642 // KTextEdit used to take ownership of the highlighter, Sonnet::SpellCheckDecorator does not.
643 // so we reparent the highlighter so it will be deleted when the decorator is destroyed
644 _highLighter->setParent(decorator);
645 addTextDecorator(decorator);
646}
647
648void KTextEdit::setCheckSpellingEnabled(bool check)
649{
650 Q_D(KTextEdit);
651
652 Q_EMIT checkSpellingChanged(check);
653 if (check == d->spellCheckingEnabled) {
654 return;
655 }
656
657 // From the above statement we now know that if we're turning checking
658 // on that we need to create a new highlighter and if we're turning it
659 // off we should remove the old one.
660
661 d->spellCheckingEnabled = check;
662 if (check) {
663 if (hasFocus()) {
664 createHighlighter();
665 if (!spellCheckingLanguage().isEmpty()) {
666 setSpellCheckingLanguage(spellCheckingLanguage());
667 }
668 }
669 } else {
670 clearDecorator();
671 }
672}
673
674void KTextEdit::focusInEvent(QFocusEvent *event)
675{
676 Q_D(KTextEdit);
677
678 if (d->spellCheckingEnabled && !isReadOnly() && !d->decorator) {
679 createHighlighter();
680 }
681
682 QTextEdit::focusInEvent(e: event);
683}
684
685bool KTextEdit::checkSpellingEnabled() const
686{
687 Q_D(const KTextEdit);
688
689 return d->spellCheckingEnabled;
690}
691
692bool KTextEdit::shouldBlockBeSpellChecked(const QString &) const
693{
694 return true;
695}
696
697void KTextEdit::setReadOnly(bool readOnly)
698{
699 Q_D(KTextEdit);
700
701 if (!readOnly && hasFocus() && d->spellCheckingEnabled && !d->decorator) {
702 createHighlighter();
703 }
704
705 if (readOnly == isReadOnly()) {
706 return;
707 }
708
709 if (readOnly) {
710 // Set pointer to null before deleting KTextDecorator as dtor will emit signal,
711 // which could call this code again and cause double delete/crash
712 auto decorator = d->decorator;
713 d->decorator = nullptr;
714 delete decorator;
715
716 d->customPalette = testAttribute(attribute: Qt::WA_SetPalette);
717 QPalette p = palette();
718 QColor color = p.color(cg: QPalette::Disabled, cr: QPalette::Window);
719 p.setColor(acr: QPalette::Base, acolor: color);
720 p.setColor(acr: QPalette::Window, acolor: color);
721 setPalette(p);
722 } else {
723 if (d->customPalette && testAttribute(attribute: Qt::WA_SetPalette)) {
724 QPalette p = palette();
725 QColor color = p.color(cg: QPalette::Normal, cr: QPalette::Base);
726 p.setColor(acr: QPalette::Base, acolor: color);
727 p.setColor(acr: QPalette::Window, acolor: color);
728 setPalette(p);
729 } else {
730 setPalette(QPalette());
731 }
732 }
733
734 QTextEdit::setReadOnly(readOnly);
735}
736
737void KTextEdit::checkSpelling()
738{
739 Q_D(KTextEdit);
740
741 d->checkSpelling(force: false);
742}
743
744void KTextEdit::forceSpellChecking()
745{
746 Q_D(KTextEdit);
747
748 d->checkSpelling(force: true);
749}
750
751void KTextEdit::highlightWord(int length, int pos)
752{
753 QTextCursor cursor(document());
754 cursor.setPosition(pos);
755 cursor.setPosition(pos: pos + length, mode: QTextCursor::KeepAnchor);
756 setTextCursor(cursor);
757 ensureCursorVisible();
758}
759
760void KTextEdit::replace()
761{
762 Q_D(KTextEdit);
763
764 if (document()->isEmpty()) { // saves having to track the text changes
765 return;
766 }
767
768 if (d->repDlg) {
769 d->repDlg->activateWindow();
770 } else {
771 d->repDlg = new KReplaceDialog(this, 0, QStringList(), QStringList(), false);
772 connect(sender: d->repDlg, signal: &KFindDialog::okClicked, context: this, slot: &KTextEdit::slotDoReplace);
773 }
774 d->repDlg->show();
775}
776
777void KTextEdit::slotDoReplace()
778{
779 Q_D(KTextEdit);
780
781 if (!d->repDlg) {
782 // Should really assert()
783 return;
784 }
785
786 if (d->repDlg->pattern().isEmpty()) {
787 delete d->replace;
788 d->replace = nullptr;
789 ensureCursorVisible();
790 return;
791 }
792
793 delete d->replace;
794 d->replace = new KReplace(d->repDlg->pattern(), d->repDlg->replacement(), d->repDlg->options(), this);
795 d->repIndex = 0;
796 if (d->replace->options() & KFind::FromCursor || d->replace->options() & KFind::FindBackwards) {
797 d->repIndex = textCursor().anchor();
798 }
799
800 // Connect textFound signal to code which handles highlighting of found text.
801 connect(sender: d->replace, signal: &KFind::textFound, context: this, slot: [d](const QString &text, int matchingIndex, int matchedLength) {
802 d->slotFindHighlight(text, matchingIndex, matchingLength: matchedLength);
803 });
804 connect(sender: d->replace, signal: &KFind::findNext, context: this, slot: &KTextEdit::slotReplaceNext);
805
806 connect(sender: d->replace, signal: &KReplace::textReplaced, context: this, slot: [d](const QString &text, int replacementIndex, int replacedLength, int matchedLength) {
807 d->slotReplaceText(text, replacementIndex, replacedLength, matchedLength);
808 });
809
810 d->repDlg->close();
811 slotReplaceNext();
812}
813
814void KTextEdit::slotReplaceNext()
815{
816 Q_D(KTextEdit);
817
818 if (!d->replace) {
819 return;
820 }
821
822 d->lastReplacedPosition = -1;
823 if (!(d->replace->options() & KReplaceDialog::PromptOnReplace)) {
824 textCursor().beginEditBlock(); // #48541
825 viewport()->setUpdatesEnabled(false);
826 }
827
828 if (d->replace->needData()) {
829 d->replace->setData(data: toPlainText(), startPos: d->repIndex);
830 }
831 KFind::Result res = d->replace->replace();
832 if (!(d->replace->options() & KReplaceDialog::PromptOnReplace)) {
833 textCursor().endEditBlock(); // #48541
834 if (d->lastReplacedPosition >= 0) {
835 QTextCursor tc = textCursor();
836 tc.setPosition(pos: d->lastReplacedPosition);
837 setTextCursor(tc);
838 ensureCursorVisible();
839 }
840
841 viewport()->setUpdatesEnabled(true);
842 viewport()->update();
843 }
844
845 if (res == KFind::NoMatch) {
846 d->replace->displayFinalDialog();
847 d->replace->disconnect(receiver: this);
848 d->replace->deleteLater(); // we are in a slot connected to m_replace, don't delete it right away
849 d->replace = nullptr;
850 ensureCursorVisible();
851 // or if ( m_replace->shouldRestart() ) { reinit (w/o FromCursor) and call slotReplaceNext(); }
852 } else {
853 // m_replace->closeReplaceNextDialog();
854 }
855}
856
857void KTextEdit::slotDoFind()
858{
859 Q_D(KTextEdit);
860
861 if (!d->findDlg) {
862 // Should really assert()
863 return;
864 }
865 if (d->findDlg->pattern().isEmpty()) {
866 delete d->find;
867 d->find = nullptr;
868 return;
869 }
870 delete d->find;
871 d->find = new KFind(d->findDlg->pattern(), d->findDlg->options(), this);
872 d->findIndex = 0;
873 if (d->find->options() & KFind::FromCursor || d->find->options() & KFind::FindBackwards) {
874 d->findIndex = textCursor().anchor();
875 }
876
877 // Connect textFound() signal to code which handles highlighting of found text
878 connect(sender: d->find, signal: &KFind::textFound, context: this, slot: [d](const QString &text, int matchingIndex, int matchedLength) {
879 d->slotFindHighlight(text, matchingIndex, matchingLength: matchedLength);
880 });
881 connect(sender: d->find, signal: &KFind::findNext, context: this, slot: &KTextEdit::slotFindNext);
882
883 d->findDlg->close();
884 d->find->closeFindNextDialog();
885 slotFindNext();
886}
887
888void KTextEdit::slotFindNext()
889{
890 Q_D(KTextEdit);
891
892 if (!d->find) {
893 return;
894 }
895 if (document()->isEmpty()) {
896 d->find->disconnect(receiver: this);
897 d->find->deleteLater(); // we are in a slot connected to m_find, don't delete right away
898 d->find = nullptr;
899 return;
900 }
901
902 if (d->find->needData()) {
903 d->find->setData(data: toPlainText(), startPos: d->findIndex);
904 }
905 KFind::Result res = d->find->find();
906
907 if (res == KFind::NoMatch) {
908 d->find->displayFinalDialog();
909 d->find->disconnect(receiver: this);
910 d->find->deleteLater(); // we are in a slot connected to m_find, don't delete right away
911 d->find = nullptr;
912 // or if ( m_find->shouldRestart() ) { reinit (w/o FromCursor) and call slotFindNext(); }
913 } else {
914 // m_find->closeFindNextDialog();
915 }
916}
917
918void KTextEdit::slotFindPrevious()
919{
920 Q_D(KTextEdit);
921
922 if (!d->find) {
923 return;
924 }
925 const long oldOptions = d->find->options();
926 d->find->setOptions(oldOptions ^ KFind::FindBackwards);
927 slotFindNext();
928 if (d->find) {
929 d->find->setOptions(oldOptions);
930 }
931}
932
933void KTextEdit::slotFind()
934{
935 Q_D(KTextEdit);
936
937 if (document()->isEmpty()) { // saves having to track the text changes
938 return;
939 }
940
941 if (d->findDlg) {
942 d->findDlg->activateWindow();
943 } else {
944 d->findDlg = new KFindDialog(this);
945 connect(sender: d->findDlg, signal: &KFindDialog::okClicked, context: this, slot: &KTextEdit::slotDoFind);
946 }
947 d->findDlg->show();
948}
949
950void KTextEdit::slotReplace()
951{
952 Q_D(KTextEdit);
953
954 if (document()->isEmpty()) { // saves having to track the text changes
955 return;
956 }
957
958 if (d->repDlg) {
959 d->repDlg->activateWindow();
960 } else {
961 d->repDlg = new KReplaceDialog(this, 0, QStringList(), QStringList(), false);
962 connect(sender: d->repDlg, signal: &KFindDialog::okClicked, context: this, slot: &KTextEdit::slotDoReplace);
963 }
964 d->repDlg->show();
965}
966
967void KTextEdit::enableFindReplace(bool enabled)
968{
969 Q_D(KTextEdit);
970
971 d->findReplaceEnabled = enabled;
972}
973
974void KTextEdit::showTabAction(bool show)
975{
976 Q_D(KTextEdit);
977
978 d->showTabAction = show;
979}
980
981bool KTextEditPrivate::overrideShortcut(const QKeyEvent *event)
982{
983 const int key = event->key() | event->modifiers();
984
985 if (KStandardShortcut::copy().contains(t: key)) {
986 return true;
987 } else if (KStandardShortcut::paste().contains(t: key)) {
988 return true;
989 } else if (KStandardShortcut::cut().contains(t: key)) {
990 return true;
991 } else if (KStandardShortcut::undo().contains(t: key)) {
992 return true;
993 } else if (KStandardShortcut::redo().contains(t: key)) {
994 return true;
995 } else if (KStandardShortcut::deleteWordBack().contains(t: key)) {
996 return true;
997 } else if (KStandardShortcut::deleteWordForward().contains(t: key)) {
998 return true;
999 } else if (KStandardShortcut::backwardWord().contains(t: key)) {
1000 return true;
1001 } else if (KStandardShortcut::forwardWord().contains(t: key)) {
1002 return true;
1003 } else if (KStandardShortcut::next().contains(t: key)) {
1004 return true;
1005 } else if (KStandardShortcut::prior().contains(t: key)) {
1006 return true;
1007 } else if (KStandardShortcut::begin().contains(t: key)) {
1008 return true;
1009 } else if (KStandardShortcut::end().contains(t: key)) {
1010 return true;
1011 } else if (KStandardShortcut::beginningOfLine().contains(t: key)) {
1012 return true;
1013 } else if (KStandardShortcut::endOfLine().contains(t: key)) {
1014 return true;
1015 } else if (KStandardShortcut::pasteSelection().contains(t: key)) {
1016 return true;
1017 } else if (findReplaceEnabled && KStandardShortcut::find().contains(t: key)) {
1018 return true;
1019 } else if (findReplaceEnabled && KStandardShortcut::findNext().contains(t: key)) {
1020 return true;
1021 } else if (findReplaceEnabled && KStandardShortcut::findPrev().contains(t: key)) {
1022 return true;
1023 } else if (findReplaceEnabled && KStandardShortcut::replace().contains(t: key)) {
1024 return true;
1025 } else if (event->matches(key: QKeySequence::SelectAll)) { // currently missing in QTextEdit
1026 return true;
1027 }
1028 return false;
1029}
1030
1031void KTextEdit::keyPressEvent(QKeyEvent *event)
1032{
1033 Q_D(KTextEdit);
1034
1035 if (d->handleShortcut(event)) {
1036 event->accept();
1037 } else {
1038 QTextEdit::keyPressEvent(e: event);
1039 }
1040}
1041
1042void KTextEdit::showAutoCorrectButton(bool show)
1043{
1044 Q_D(KTextEdit);
1045
1046 d->showAutoCorrectionButton = show;
1047}
1048
1049#include "moc_ktextedit.cpp"
1050

source code of ktextwidgets/src/widgets/ktextedit.cpp