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 | |
32 | class KTextDecorator : public Sonnet::SpellCheckDecorator |
33 | { |
34 | public: |
35 | explicit KTextDecorator(KTextEdit *textEdit); |
36 | bool isSpellCheckingEnabledForBlock(const QString &textBlock) const override; |
37 | |
38 | private: |
39 | KTextEdit *m_textEdit; |
40 | }; |
41 | |
42 | void 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 | |
91 | void 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 | |
102 | void KTextEditPrivate::spellCheckerAutoCorrect(const QString ¤tWord, const QString &autoCorrectWord) |
103 | { |
104 | Q_Q(KTextEdit); |
105 | |
106 | Q_EMIT q->spellCheckerAutoCorrect(currentWord, autoCorrectWord); |
107 | } |
108 | |
109 | void 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 | |
117 | void 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 | |
130 | void 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 | |
142 | void KTextEditPrivate::toggleAutoSpellCheck() |
143 | { |
144 | Q_Q(KTextEdit); |
145 | |
146 | q->setCheckSpellingEnabled(!q->checkSpellingEnabled()); |
147 | } |
148 | |
149 | void 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 | |
161 | void KTextEditPrivate::slotAllowTab() |
162 | { |
163 | Q_Q(KTextEdit); |
164 | |
165 | q->setTabChangesFocus(!q->tabChangesFocus()); |
166 | } |
167 | |
168 | void KTextEditPrivate::(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 | |
181 | void 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 | |
194 | void 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 | |
211 | void 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 | |
219 | KTextDecorator::KTextDecorator(KTextEdit *textEdit) |
220 | : SpellCheckDecorator(textEdit) |
221 | , m_textEdit(textEdit) |
222 | { |
223 | } |
224 | |
225 | bool KTextDecorator::isSpellCheckingEnabledForBlock(const QString &textBlock) const |
226 | { |
227 | return m_textEdit->shouldBlockBeSpellChecked(block: textBlock); |
228 | } |
229 | |
230 | KTextEdit::KTextEdit(const QString &text, QWidget *parent) |
231 | : KTextEdit(*new KTextEditPrivate(this), text, parent) |
232 | { |
233 | } |
234 | |
235 | KTextEdit::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 | |
244 | KTextEdit::KTextEdit(QWidget *parent) |
245 | : KTextEdit(*new KTextEditPrivate(this), parent) |
246 | { |
247 | } |
248 | |
249 | KTextEdit::KTextEdit(KTextEditPrivate &dd, QWidget *parent) |
250 | : QTextEdit(parent) |
251 | , d_ptr(&dd) |
252 | { |
253 | Q_D(KTextEdit); |
254 | |
255 | d->init(); |
256 | } |
257 | |
258 | KTextEdit::~KTextEdit() = default; |
259 | |
260 | const QString &KTextEdit::spellCheckingLanguage() const |
261 | { |
262 | Q_D(const KTextEdit); |
263 | |
264 | return d->spellCheckingLanguage; |
265 | } |
266 | |
267 | void 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 | |
282 | void 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 | |
298 | bool 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 | |
312 | bool 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 | |
439 | static void deleteWord(QTextCursor cursor, QTextCursor::MoveOperation op) |
440 | { |
441 | cursor.clearSelection(); |
442 | cursor.movePosition(op, QTextCursor::KeepAnchor); |
443 | cursor.removeSelectedText(); |
444 | } |
445 | |
446 | void 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 | |
453 | void 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 | |
460 | QMenu *KTextEdit::() |
461 | { |
462 | Q_D(KTextEdit); |
463 | |
464 | QMenu * = 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 | |
574 | void 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 | |
592 | void KTextEdit::(QContextMenuEvent *event) |
593 | { |
594 | QMenu * = mousePopupMenu(); |
595 | if (popup) { |
596 | aboutToShowContextMenu(menu: popup); |
597 | popup->exec(pos: event->globalPos()); |
598 | delete popup; |
599 | } |
600 | } |
601 | |
602 | void KTextEdit::createHighlighter() |
603 | { |
604 | setHighlighter(new Sonnet::Highlighter(this)); |
605 | } |
606 | |
607 | Sonnet::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 | |
618 | void 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 | |
629 | void KTextEdit::addTextDecorator(Sonnet::SpellCheckDecorator *decorator) |
630 | { |
631 | Q_D(KTextEdit); |
632 | |
633 | d->decorator = decorator; |
634 | } |
635 | |
636 | void 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 | |
649 | void 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 | |
675 | void 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 | |
686 | bool KTextEdit::checkSpellingEnabled() const |
687 | { |
688 | Q_D(const KTextEdit); |
689 | |
690 | return d->spellCheckingEnabled; |
691 | } |
692 | |
693 | bool KTextEdit::shouldBlockBeSpellChecked(const QString &) const |
694 | { |
695 | return true; |
696 | } |
697 | |
698 | void 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 | |
738 | void KTextEdit::checkSpelling() |
739 | { |
740 | Q_D(KTextEdit); |
741 | |
742 | d->checkSpelling(force: false); |
743 | } |
744 | |
745 | void KTextEdit::forceSpellChecking() |
746 | { |
747 | Q_D(KTextEdit); |
748 | |
749 | d->checkSpelling(force: true); |
750 | } |
751 | |
752 | void 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 | |
761 | void 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 | |
778 | void 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 | |
815 | void 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 | |
858 | void 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 | |
889 | void 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 | |
919 | void 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 | |
934 | void 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 | |
951 | void 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 | |
968 | void KTextEdit::enableFindReplace(bool enabled) |
969 | { |
970 | Q_D(KTextEdit); |
971 | |
972 | d->findReplaceEnabled = enabled; |
973 | } |
974 | |
975 | void KTextEdit::showTabAction(bool show) |
976 | { |
977 | Q_D(KTextEdit); |
978 | |
979 | d->showTabAction = show; |
980 | } |
981 | |
982 | bool 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 | |
1032 | void 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 | |
1043 | void KTextEdit::showAutoCorrectButton(bool show) |
1044 | { |
1045 | Q_D(KTextEdit); |
1046 | |
1047 | d->showAutoCorrectionButton = show; |
1048 | } |
1049 | |
1050 | #include "moc_ktextedit.cpp" |
1051 | |