1/*
2 * spellcheckdecorator.h
3 *
4 * SPDX-FileCopyrightText: 2013 Aurélien Gâteau <agateau@kde.org>
5 *
6 * SPDX-License-Identifier: LGPL-2.1-or-later
7 */
8#include "spellcheckdecorator.h"
9
10// Local
11#include <highlighter.h>
12
13// Qt
14#include <QContextMenuEvent>
15#include <QMenu>
16#include <QPlainTextEdit>
17#include <QTextEdit>
18
19namespace Sonnet
20{
21class SpellCheckDecoratorPrivate
22{
23public:
24 SpellCheckDecoratorPrivate(SpellCheckDecorator *installer, QPlainTextEdit *textEdit)
25 : q(installer)
26 , m_plainTextEdit(textEdit)
27 {
28 createDefaultHighlighter();
29 // Catch pressing the "menu" key
30 m_plainTextEdit->installEventFilter(filterObj: q);
31 // Catch right-click
32 m_plainTextEdit->viewport()->installEventFilter(filterObj: q);
33 }
34
35 SpellCheckDecoratorPrivate(SpellCheckDecorator *installer, QTextEdit *textEdit)
36 : q(installer)
37 , m_textEdit(textEdit)
38 {
39 createDefaultHighlighter();
40 // Catch pressing the "menu" key
41 m_textEdit->installEventFilter(filterObj: q);
42 // Catch right-click
43 m_textEdit->viewport()->installEventFilter(filterObj: q);
44 }
45
46 ~SpellCheckDecoratorPrivate() = default;
47
48 void removeEventFilters()
49 {
50 if (m_plainTextEdit) {
51 m_plainTextEdit->removeEventFilter(obj: q);
52 m_plainTextEdit->viewport()->removeEventFilter(obj: q);
53 }
54 if (m_textEdit) {
55 m_textEdit->removeEventFilter(obj: q);
56 m_textEdit->viewport()->removeEventFilter(obj: q);
57 }
58 }
59
60 void onParentDestroyed()
61 {
62 m_plainTextEdit = nullptr;
63 m_textEdit = nullptr;
64 }
65
66 bool onContextMenuEvent(QContextMenuEvent *event);
67 void execSuggestionMenu(const QPoint &pos, const QString &word, const QTextCursor &cursor);
68 void createDefaultHighlighter();
69
70 SpellCheckDecorator *const q;
71 QTextEdit *m_textEdit = nullptr;
72 QPlainTextEdit *m_plainTextEdit = nullptr;
73 Highlighter *m_highlighter = nullptr;
74};
75
76bool SpellCheckDecoratorPrivate::onContextMenuEvent(QContextMenuEvent *event)
77{
78 if (!m_highlighter) {
79 createDefaultHighlighter();
80 }
81
82 // Obtain the cursor at the mouse position and the current cursor
83 QTextCursor cursorAtMouse;
84 if (m_textEdit) {
85 cursorAtMouse = m_textEdit->cursorForPosition(pos: event->pos());
86 } else {
87 cursorAtMouse = m_plainTextEdit->cursorForPosition(pos: event->pos());
88 }
89 const int mousePos = cursorAtMouse.position();
90 QTextCursor cursor;
91 if (m_textEdit) {
92 cursor = m_textEdit->textCursor();
93 } else {
94 cursor = m_plainTextEdit->textCursor();
95 }
96
97 // Check if the user clicked a selected word
98 /* clang-format off */
99 const bool selectedWordClicked = cursor.hasSelection()
100 && mousePos >= cursor.selectionStart()
101 && mousePos <= cursor.selectionEnd();
102 /* clang-format on */
103
104 // Get the word under the (mouse-)cursor and see if it is misspelled.
105 // Don't include apostrophes at the start/end of the word in the selection.
106 QTextCursor wordSelectCursor(cursorAtMouse);
107 wordSelectCursor.clearSelection();
108 wordSelectCursor.select(selection: QTextCursor::WordUnderCursor);
109 QString selectedWord = wordSelectCursor.selectedText();
110
111 bool isMouseCursorInsideWord = true;
112 if ((mousePos < wordSelectCursor.selectionStart() || mousePos >= wordSelectCursor.selectionEnd()) //
113 && (selectedWord.length() > 1)) {
114 isMouseCursorInsideWord = false;
115 }
116
117 // Clear the selection again, we re-select it below (without the apostrophes).
118 wordSelectCursor.setPosition(pos: wordSelectCursor.position() - selectedWord.size());
119 if (selectedWord.startsWith(c: QLatin1Char('\'')) || selectedWord.startsWith(c: QLatin1Char('\"'))) {
120 selectedWord = selectedWord.right(n: selectedWord.size() - 1);
121 wordSelectCursor.movePosition(op: QTextCursor::NextCharacter, QTextCursor::MoveAnchor);
122 }
123 if (selectedWord.endsWith(c: QLatin1Char('\'')) || selectedWord.endsWith(c: QLatin1Char('\"'))) {
124 selectedWord.chop(n: 1);
125 }
126
127 wordSelectCursor.movePosition(op: QTextCursor::NextCharacter, QTextCursor::KeepAnchor, n: selectedWord.size());
128
129 /* clang-format off */
130 const bool wordIsMisspelled = isMouseCursorInsideWord
131 && m_highlighter
132 && m_highlighter->isActive()
133 && !selectedWord.isEmpty()
134 && m_highlighter->isWordMisspelled(word: selectedWord);
135 /* clang-format on */
136
137 // If the user clicked a selected word, do nothing.
138 // If the user clicked somewhere else, move the cursor there.
139 // If the user clicked on a misspelled word, select that word.
140 // Same behavior as in OpenOffice Writer.
141 bool checkBlock = q->isSpellCheckingEnabledForBlock(textBlock: cursorAtMouse.block().text());
142 if (!selectedWordClicked) {
143 if (wordIsMisspelled && checkBlock) {
144 if (m_textEdit) {
145 m_textEdit->setTextCursor(wordSelectCursor);
146 } else {
147 m_plainTextEdit->setTextCursor(wordSelectCursor);
148 }
149 } else {
150 if (m_textEdit) {
151 m_textEdit->setTextCursor(cursorAtMouse);
152 } else {
153 m_plainTextEdit->setTextCursor(cursorAtMouse);
154 }
155 }
156 if (m_textEdit) {
157 cursor = m_textEdit->textCursor();
158 } else {
159 cursor = m_plainTextEdit->textCursor();
160 }
161 }
162
163 // Use standard context menu for already selected words, correctly spelled
164 // words and words inside quotes.
165 if (!wordIsMisspelled || selectedWordClicked || !checkBlock) {
166 return false;
167 }
168 execSuggestionMenu(pos: event->globalPos(), word: selectedWord, cursor);
169 return true;
170}
171
172void SpellCheckDecoratorPrivate::execSuggestionMenu(const QPoint &pos, const QString &selectedWord, const QTextCursor &_cursor)
173{
174 QTextCursor cursor = _cursor;
175 QMenu menu; // don't use KMenu here we don't want auto management accelerator
176
177 // Add the suggestions to the menu
178 const QStringList reps = m_highlighter->suggestionsForWord(word: selectedWord, cursor);
179 if (reps.isEmpty()) {
180 QAction *suggestionsAction = menu.addAction(text: SpellCheckDecorator::tr(s: "No suggestions for %1").arg(a: selectedWord));
181 suggestionsAction->setEnabled(false);
182 } else {
183 QStringList::const_iterator end(reps.constEnd());
184 for (QStringList::const_iterator it = reps.constBegin(); it != end; ++it) {
185 menu.addAction(text: *it);
186 }
187 }
188
189 menu.addSeparator();
190
191 QAction *ignoreAction = menu.addAction(text: SpellCheckDecorator::tr(s: "Ignore"));
192 QAction *addToDictAction = menu.addAction(text: SpellCheckDecorator::tr(s: "Add to Dictionary"));
193 // Execute the popup inline
194 const QAction *selectedAction = menu.exec(pos);
195
196 if (selectedAction) {
197 // Fails when we're in the middle of a compose-key sequence
198 // Q_ASSERT(cursor.selectedText() == selectedWord);
199
200 if (selectedAction == ignoreAction) {
201 m_highlighter->ignoreWord(word: selectedWord);
202 m_highlighter->rehighlight();
203 } else if (selectedAction == addToDictAction) {
204 m_highlighter->addWordToDictionary(word: selectedWord);
205 m_highlighter->rehighlight();
206 }
207 // Other actions can only be one of the suggested words
208 else {
209 const QString replacement = selectedAction->text();
210 Q_ASSERT(reps.contains(replacement));
211 cursor.insertText(text: replacement);
212 if (m_textEdit) {
213 m_textEdit->setTextCursor(cursor);
214 } else {
215 m_plainTextEdit->setTextCursor(cursor);
216 }
217 }
218 }
219}
220
221void SpellCheckDecoratorPrivate::createDefaultHighlighter()
222{
223 if (m_textEdit) {
224 m_highlighter = new Highlighter(m_textEdit);
225 } else {
226 m_highlighter = new Highlighter(m_plainTextEdit);
227 }
228}
229
230SpellCheckDecorator::SpellCheckDecorator(QTextEdit *textEdit)
231 : QObject(textEdit)
232 , d(std::make_unique<SpellCheckDecoratorPrivate>(args: this, args&: textEdit))
233{
234 connect(sender: textEdit, signal: &QObject::destroyed, context: this, slot: [this] {
235 d->onParentDestroyed();
236 });
237}
238
239SpellCheckDecorator::SpellCheckDecorator(QPlainTextEdit *textEdit)
240 : QObject(textEdit)
241 , d(std::make_unique<SpellCheckDecoratorPrivate>(args: this, args&: textEdit))
242{
243 connect(sender: textEdit, signal: &QObject::destroyed, context: this, slot: [this] {
244 d->onParentDestroyed();
245 });
246}
247
248SpellCheckDecorator::~SpellCheckDecorator()
249{
250 d->removeEventFilters();
251}
252
253void SpellCheckDecorator::setHighlighter(Highlighter *highlighter)
254{
255 d->m_highlighter = highlighter;
256}
257
258Highlighter *SpellCheckDecorator::highlighter() const
259{
260 if (!d->m_highlighter) {
261 d->createDefaultHighlighter();
262 }
263 return d->m_highlighter;
264}
265
266bool SpellCheckDecorator::eventFilter(QObject * /*obj*/, QEvent *event)
267{
268 if (event->type() == QEvent::ContextMenu) {
269 return d->onContextMenuEvent(event: static_cast<QContextMenuEvent *>(event));
270 }
271 return false;
272}
273
274bool SpellCheckDecorator::isSpellCheckingEnabledForBlock(const QString &textBlock) const
275{
276 Q_UNUSED(textBlock);
277 if (d->m_textEdit) {
278 return d->m_textEdit->isEnabled();
279 } else {
280 return d->m_plainTextEdit->isEnabled();
281 }
282}
283} // namespace
284
285#include "moc_spellcheckdecorator.cpp"
286

source code of sonnet/src/ui/spellcheckdecorator.cpp