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 <KStandardAction>
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 = KStandardAction::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")), i18n("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 = KStandardAction::find(recvr: this, slot: &KTextEdit::slotFind, parent: popup);
541 QAction *findNextAction = KStandardAction::findNext(recvr: this, slot: &KTextEdit::slotFindNext, parent: popup);
542 QAction *findPrevAction = KStandardAction::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 = KStandardAction::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 Q_D(KTextEdit);
577
578#ifdef HAVE_SPEECH
579 QString text;
580 if (textCursor().hasSelection()) {
581 text = textCursor().selectedText();
582 } else {
583 text = toPlainText();
584 }
585 if (!d->textToSpeech) {
586 d->textToSpeech = new QTextToSpeech(this);
587 }
588 d->textToSpeech->say(text);
589#endif
590}
591
592void KTextEdit::contextMenuEvent(QContextMenuEvent *event)
593{
594 QMenu *popup = mousePopupMenu();
595 if (popup) {
596 aboutToShowContextMenu(menu: popup);
597 popup->exec(pos: event->globalPos());
598 delete popup;
599 }
600}
601
602void KTextEdit::createHighlighter()
603{
604 setHighlighter(new Sonnet::Highlighter(this));
605}
606
607Sonnet::Highlighter *KTextEdit::highlighter() const
608{
609 Q_D(const KTextEdit);
610
611 if (d->decorator) {
612 return d->decorator->highlighter();
613 } else {
614 return nullptr;
615 }
616}
617
618void KTextEdit::clearDecorator()
619{
620 Q_D(KTextEdit);
621
622 // Set pointer to null before deleting KTextDecorator as dtor will emit signal,
623 // which could call this code again and cause double delete/crash
624 auto decorator = d->decorator;
625 d->decorator = nullptr;
626 delete decorator;
627}
628
629void KTextEdit::addTextDecorator(Sonnet::SpellCheckDecorator *decorator)
630{
631 Q_D(KTextEdit);
632
633 d->decorator = decorator;
634}
635
636void KTextEdit::setHighlighter(Sonnet::Highlighter *_highLighter)
637{
638 KTextDecorator *decorator = new KTextDecorator(this);
639 // The old default highlighter must be manually deleted.
640 delete decorator->highlighter();
641 decorator->setHighlighter(_highLighter);
642
643 // KTextEdit used to take ownership of the highlighter, Sonnet::SpellCheckDecorator does not.
644 // so we reparent the highlighter so it will be deleted when the decorator is destroyed
645 _highLighter->setParent(decorator);
646 addTextDecorator(decorator);
647}
648
649void KTextEdit::setCheckSpellingEnabled(bool check)
650{
651 Q_D(KTextEdit);
652
653 Q_EMIT checkSpellingChanged(check);
654 if (check == d->spellCheckingEnabled) {
655 return;
656 }
657
658 // From the above statement we now know that if we're turning checking
659 // on that we need to create a new highlighter and if we're turning it
660 // off we should remove the old one.
661
662 d->spellCheckingEnabled = check;
663 if (check) {
664 if (hasFocus()) {
665 createHighlighter();
666 if (!spellCheckingLanguage().isEmpty()) {
667 setSpellCheckingLanguage(spellCheckingLanguage());
668 }
669 }
670 } else {
671 clearDecorator();
672 }
673}
674
675void KTextEdit::focusInEvent(QFocusEvent *event)
676{
677 Q_D(KTextEdit);
678
679 if (d->spellCheckingEnabled && !isReadOnly() && !d->decorator) {
680 createHighlighter();
681 }
682
683 QTextEdit::focusInEvent(e: event);
684}
685
686bool KTextEdit::checkSpellingEnabled() const
687{
688 Q_D(const KTextEdit);
689
690 return d->spellCheckingEnabled;
691}
692
693bool KTextEdit::shouldBlockBeSpellChecked(const QString &) const
694{
695 return true;
696}
697
698void KTextEdit::setReadOnly(bool readOnly)
699{
700 Q_D(KTextEdit);
701
702 if (!readOnly && hasFocus() && d->spellCheckingEnabled && !d->decorator) {
703 createHighlighter();
704 }
705
706 if (readOnly == isReadOnly()) {
707 return;
708 }
709
710 if (readOnly) {
711 // Set pointer to null before deleting KTextDecorator as dtor will emit signal,
712 // which could call this code again and cause double delete/crash
713 auto decorator = d->decorator;
714 d->decorator = nullptr;
715 delete decorator;
716
717 d->customPalette = testAttribute(attribute: Qt::WA_SetPalette);
718 QPalette p = palette();
719 QColor color = p.color(cg: QPalette::Disabled, cr: QPalette::Window);
720 p.setColor(acr: QPalette::Base, acolor: color);
721 p.setColor(acr: QPalette::Window, acolor: color);
722 setPalette(p);
723 } else {
724 if (d->customPalette && testAttribute(attribute: Qt::WA_SetPalette)) {
725 QPalette p = palette();
726 QColor color = p.color(cg: QPalette::Normal, cr: QPalette::Base);
727 p.setColor(acr: QPalette::Base, acolor: color);
728 p.setColor(acr: QPalette::Window, acolor: color);
729 setPalette(p);
730 } else {
731 setPalette(QPalette());
732 }
733 }
734
735 QTextEdit::setReadOnly(readOnly);
736}
737
738void KTextEdit::checkSpelling()
739{
740 Q_D(KTextEdit);
741
742 d->checkSpelling(force: false);
743}
744
745void KTextEdit::forceSpellChecking()
746{
747 Q_D(KTextEdit);
748
749 d->checkSpelling(force: true);
750}
751
752void KTextEdit::highlightWord(int length, int pos)
753{
754 QTextCursor cursor(document());
755 cursor.setPosition(pos);
756 cursor.setPosition(pos: pos + length, mode: QTextCursor::KeepAnchor);
757 setTextCursor(cursor);
758 ensureCursorVisible();
759}
760
761void KTextEdit::replace()
762{
763 Q_D(KTextEdit);
764
765 if (document()->isEmpty()) { // saves having to track the text changes
766 return;
767 }
768
769 if (d->repDlg) {
770 d->repDlg->activateWindow();
771 } else {
772 d->repDlg = new KReplaceDialog(this, 0, QStringList(), QStringList(), false);
773 connect(sender: d->repDlg, signal: &KFindDialog::okClicked, context: this, slot: &KTextEdit::slotDoReplace);
774 }
775 d->repDlg->show();
776}
777
778void KTextEdit::slotDoReplace()
779{
780 Q_D(KTextEdit);
781
782 if (!d->repDlg) {
783 // Should really assert()
784 return;
785 }
786
787 if (d->repDlg->pattern().isEmpty()) {
788 delete d->replace;
789 d->replace = nullptr;
790 ensureCursorVisible();
791 return;
792 }
793
794 delete d->replace;
795 d->replace = new KReplace(d->repDlg->pattern(), d->repDlg->replacement(), d->repDlg->options(), this);
796 d->repIndex = 0;
797 if (d->replace->options() & KFind::FromCursor || d->replace->options() & KFind::FindBackwards) {
798 d->repIndex = textCursor().anchor();
799 }
800
801 // Connect textFound signal to code which handles highlighting of found text.
802 connect(sender: d->replace, signal: &KFind::textFound, context: this, slot: [d](const QString &text, int matchingIndex, int matchedLength) {
803 d->slotFindHighlight(text, matchingIndex, matchingLength: matchedLength);
804 });
805 connect(sender: d->replace, signal: &KFind::findNext, context: this, slot: &KTextEdit::slotReplaceNext);
806
807 connect(sender: d->replace, signal: &KReplace::textReplaced, context: this, slot: [d](const QString &text, int replacementIndex, int replacedLength, int matchedLength) {
808 d->slotReplaceText(text, replacementIndex, replacedLength, matchedLength);
809 });
810
811 d->repDlg->close();
812 slotReplaceNext();
813}
814
815void KTextEdit::slotReplaceNext()
816{
817 Q_D(KTextEdit);
818
819 if (!d->replace) {
820 return;
821 }
822
823 d->lastReplacedPosition = -1;
824 if (!(d->replace->options() & KReplaceDialog::PromptOnReplace)) {
825 textCursor().beginEditBlock(); // #48541
826 viewport()->setUpdatesEnabled(false);
827 }
828
829 if (d->replace->needData()) {
830 d->replace->setData(data: toPlainText(), startPos: d->repIndex);
831 }
832 KFind::Result res = d->replace->replace();
833 if (!(d->replace->options() & KReplaceDialog::PromptOnReplace)) {
834 textCursor().endEditBlock(); // #48541
835 if (d->lastReplacedPosition >= 0) {
836 QTextCursor tc = textCursor();
837 tc.setPosition(pos: d->lastReplacedPosition);
838 setTextCursor(tc);
839 ensureCursorVisible();
840 }
841
842 viewport()->setUpdatesEnabled(true);
843 viewport()->update();
844 }
845
846 if (res == KFind::NoMatch) {
847 d->replace->displayFinalDialog();
848 d->replace->disconnect(receiver: this);
849 d->replace->deleteLater(); // we are in a slot connected to m_replace, don't delete it right away
850 d->replace = nullptr;
851 ensureCursorVisible();
852 // or if ( m_replace->shouldRestart() ) { reinit (w/o FromCursor) and call slotReplaceNext(); }
853 } else {
854 // m_replace->closeReplaceNextDialog();
855 }
856}
857
858void KTextEdit::slotDoFind()
859{
860 Q_D(KTextEdit);
861
862 if (!d->findDlg) {
863 // Should really assert()
864 return;
865 }
866 if (d->findDlg->pattern().isEmpty()) {
867 delete d->find;
868 d->find = nullptr;
869 return;
870 }
871 delete d->find;
872 d->find = new KFind(d->findDlg->pattern(), d->findDlg->options(), this);
873 d->findIndex = 0;
874 if (d->find->options() & KFind::FromCursor || d->find->options() & KFind::FindBackwards) {
875 d->findIndex = textCursor().anchor();
876 }
877
878 // Connect textFound() signal to code which handles highlighting of found text
879 connect(sender: d->find, signal: &KFind::textFound, context: this, slot: [d](const QString &text, int matchingIndex, int matchedLength) {
880 d->slotFindHighlight(text, matchingIndex, matchingLength: matchedLength);
881 });
882 connect(sender: d->find, signal: &KFind::findNext, context: this, slot: &KTextEdit::slotFindNext);
883
884 d->findDlg->close();
885 d->find->closeFindNextDialog();
886 slotFindNext();
887}
888
889void KTextEdit::slotFindNext()
890{
891 Q_D(KTextEdit);
892
893 if (!d->find) {
894 return;
895 }
896 if (document()->isEmpty()) {
897 d->find->disconnect(receiver: this);
898 d->find->deleteLater(); // we are in a slot connected to m_find, don't delete right away
899 d->find = nullptr;
900 return;
901 }
902
903 if (d->find->needData()) {
904 d->find->setData(data: toPlainText(), startPos: d->findIndex);
905 }
906 KFind::Result res = d->find->find();
907
908 if (res == KFind::NoMatch) {
909 d->find->displayFinalDialog();
910 d->find->disconnect(receiver: this);
911 d->find->deleteLater(); // we are in a slot connected to m_find, don't delete right away
912 d->find = nullptr;
913 // or if ( m_find->shouldRestart() ) { reinit (w/o FromCursor) and call slotFindNext(); }
914 } else {
915 // m_find->closeFindNextDialog();
916 }
917}
918
919void KTextEdit::slotFindPrevious()
920{
921 Q_D(KTextEdit);
922
923 if (!d->find) {
924 return;
925 }
926 const long oldOptions = d->find->options();
927 d->find->setOptions(oldOptions ^ KFind::FindBackwards);
928 slotFindNext();
929 if (d->find) {
930 d->find->setOptions(oldOptions);
931 }
932}
933
934void KTextEdit::slotFind()
935{
936 Q_D(KTextEdit);
937
938 if (document()->isEmpty()) { // saves having to track the text changes
939 return;
940 }
941
942 if (d->findDlg) {
943 d->findDlg->activateWindow();
944 } else {
945 d->findDlg = new KFindDialog(this);
946 connect(sender: d->findDlg, signal: &KFindDialog::okClicked, context: this, slot: &KTextEdit::slotDoFind);
947 }
948 d->findDlg->show();
949}
950
951void KTextEdit::slotReplace()
952{
953 Q_D(KTextEdit);
954
955 if (document()->isEmpty()) { // saves having to track the text changes
956 return;
957 }
958
959 if (d->repDlg) {
960 d->repDlg->activateWindow();
961 } else {
962 d->repDlg = new KReplaceDialog(this, 0, QStringList(), QStringList(), false);
963 connect(sender: d->repDlg, signal: &KFindDialog::okClicked, context: this, slot: &KTextEdit::slotDoReplace);
964 }
965 d->repDlg->show();
966}
967
968void KTextEdit::enableFindReplace(bool enabled)
969{
970 Q_D(KTextEdit);
971
972 d->findReplaceEnabled = enabled;
973}
974
975void KTextEdit::showTabAction(bool show)
976{
977 Q_D(KTextEdit);
978
979 d->showTabAction = show;
980}
981
982bool KTextEditPrivate::overrideShortcut(const QKeyEvent *event)
983{
984 const int key = event->key() | event->modifiers();
985
986 if (KStandardShortcut::copy().contains(t: key)) {
987 return true;
988 } else if (KStandardShortcut::paste().contains(t: key)) {
989 return true;
990 } else if (KStandardShortcut::cut().contains(t: key)) {
991 return true;
992 } else if (KStandardShortcut::undo().contains(t: key)) {
993 return true;
994 } else if (KStandardShortcut::redo().contains(t: key)) {
995 return true;
996 } else if (KStandardShortcut::deleteWordBack().contains(t: key)) {
997 return true;
998 } else if (KStandardShortcut::deleteWordForward().contains(t: key)) {
999 return true;
1000 } else if (KStandardShortcut::backwardWord().contains(t: key)) {
1001 return true;
1002 } else if (KStandardShortcut::forwardWord().contains(t: key)) {
1003 return true;
1004 } else if (KStandardShortcut::next().contains(t: key)) {
1005 return true;
1006 } else if (KStandardShortcut::prior().contains(t: key)) {
1007 return true;
1008 } else if (KStandardShortcut::begin().contains(t: key)) {
1009 return true;
1010 } else if (KStandardShortcut::end().contains(t: key)) {
1011 return true;
1012 } else if (KStandardShortcut::beginningOfLine().contains(t: key)) {
1013 return true;
1014 } else if (KStandardShortcut::endOfLine().contains(t: key)) {
1015 return true;
1016 } else if (KStandardShortcut::pasteSelection().contains(t: key)) {
1017 return true;
1018 } else if (findReplaceEnabled && KStandardShortcut::find().contains(t: key)) {
1019 return true;
1020 } else if (findReplaceEnabled && KStandardShortcut::findNext().contains(t: key)) {
1021 return true;
1022 } else if (findReplaceEnabled && KStandardShortcut::findPrev().contains(t: key)) {
1023 return true;
1024 } else if (findReplaceEnabled && KStandardShortcut::replace().contains(t: key)) {
1025 return true;
1026 } else if (event->matches(key: QKeySequence::SelectAll)) { // currently missing in QTextEdit
1027 return true;
1028 }
1029 return false;
1030}
1031
1032void KTextEdit::keyPressEvent(QKeyEvent *event)
1033{
1034 Q_D(KTextEdit);
1035
1036 if (d->handleShortcut(event)) {
1037 event->accept();
1038 } else {
1039 QTextEdit::keyPressEvent(e: event);
1040 }
1041}
1042
1043void KTextEdit::showAutoCorrectButton(bool show)
1044{
1045 Q_D(KTextEdit);
1046
1047 d->showAutoCorrectionButton = show;
1048}
1049
1050#include "moc_ktextedit.cpp"
1051

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