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

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