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 | |
19 | namespace Sonnet |
20 | { |
21 | class SpellCheckDecoratorPrivate |
22 | { |
23 | public: |
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 | |
68 | bool SpellCheckDecoratorPrivate::(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 | |
164 | void SpellCheckDecoratorPrivate::(const QPoint &pos, const QString &selectedWord, const QTextCursor &_cursor) |
165 | { |
166 | QTextCursor cursor = _cursor; |
167 | QMenu ; // 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 | |
213 | void 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 | |
222 | SpellCheckDecorator::SpellCheckDecorator(QTextEdit *textEdit) |
223 | : QObject(textEdit) |
224 | , d(std::make_unique<SpellCheckDecoratorPrivate>(args: this, args&: textEdit)) |
225 | { |
226 | } |
227 | |
228 | SpellCheckDecorator::SpellCheckDecorator(QPlainTextEdit *textEdit) |
229 | : QObject(textEdit) |
230 | , d(std::make_unique<SpellCheckDecoratorPrivate>(args: this, args&: textEdit)) |
231 | { |
232 | } |
233 | |
234 | SpellCheckDecorator::~SpellCheckDecorator() = default; |
235 | |
236 | void SpellCheckDecorator::setHighlighter(Highlighter *highlighter) |
237 | { |
238 | d->m_highlighter = highlighter; |
239 | } |
240 | |
241 | Highlighter *SpellCheckDecorator::highlighter() const |
242 | { |
243 | if (!d->m_highlighter) { |
244 | d->createDefaultHighlighter(); |
245 | } |
246 | return d->m_highlighter; |
247 | } |
248 | |
249 | bool 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 | |
257 | bool 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 | |