Sonnet

spellcheckdecorator.cpp
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(q);
31 // Catch right-click
32 m_plainTextEdit->viewport()->installEventFilter(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(q);
42 // Catch right-click
43 m_textEdit->viewport()->installEventFilter(q);
44 }
45
46 ~SpellCheckDecoratorPrivate()
47 {
48 if (m_plainTextEdit) {
49 m_plainTextEdit->removeEventFilter(q);
50 m_plainTextEdit->viewport()->removeEventFilter(q);
51 }
52 if (m_textEdit) {
53 m_textEdit->removeEventFilter(q);
54 m_textEdit->viewport()->removeEventFilter(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(event->pos());
78 } else {
79 cursorAtMouse = m_plainTextEdit->cursorForPosition(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(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(wordSelectCursor.position() - selectedWord.size());
111 if (selectedWord.startsWith(QLatin1Char('\'')) || selectedWord.startsWith(QLatin1Char('\"'))) {
112 selectedWord = selectedWord.right(selectedWord.size() - 1);
113 wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor);
114 }
115 if (selectedWord.endsWith(QLatin1Char('\'')) || selectedWord.endsWith(QLatin1Char('\"'))) {
116 selectedWord.chop(1);
117 }
118
119 wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 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(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(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(event->globalPos(), 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(selectedWord, cursor);
171 if (reps.isEmpty()) {
172 QAction *suggestionsAction = menu.addAction(SpellCheckDecorator::tr("No suggestions for %1").arg(selectedWord));
173 suggestionsAction->setEnabled(false);
174 } else {
176 for (QStringList::const_iterator it = reps.constBegin(); it != end; ++it) {
177 menu.addAction(*it);
178 }
179 }
180
181 menu.addSeparator();
182
183 QAction *ignoreAction = menu.addAction(SpellCheckDecorator::tr("Ignore"));
184 QAction *addToDictAction = menu.addAction(SpellCheckDecorator::tr("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(selectedWord);
194 m_highlighter->rehighlight();
195 } else if (selectedAction == addToDictAction) {
196 m_highlighter->addWordToDictionary(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(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
223 : QObject(textEdit)
224 , d(std::make_unique<SpellCheckDecoratorPrivate>(this, textEdit))
225{
226}
227
229 : QObject(textEdit)
230 , d(std::make_unique<SpellCheckDecoratorPrivate>(this, textEdit))
231{
232}
233
234SpellCheckDecorator::~SpellCheckDecorator() = default;
235
237{
238 d->m_highlighter = highlighter;
239}
240
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(static_cast<QContextMenuEvent *>(event));
253 }
254 return false;
255}
256
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"
The Sonnet Highlighter class, used for drawing pretty red lines in text fields.
Definition highlighter.h:26
void addWordToDictionary(const QString &word)
Adds the given word permanently to the dictionary.
void ignoreWord(const QString &word)
Ignores the given word.
bool isActive() const
Returns the state of spell checking.
bool isWordMisspelled(const QString &word)
Checks if a given word is marked as misspelled by the highlighter.
QStringList suggestionsForWord(const QString &word, int max=10)
Returns a list of suggested replacements for the given misspelled word.
virtual bool isSpellCheckingEnabledForBlock(const QString &textBlock) const
Returns true if the spell checking should be enabled for a given block of text The default implementa...
SpellCheckDecorator(QTextEdit *textEdit)
Creates a spell-check decorator.
Highlighter * highlighter() const
Returns the hightlighter used by the decorator.
void setHighlighter(Highlighter *highlighter)
Set a custom highlighter on the decorator.
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
The sonnet namespace.
QWidget * viewport() const const
void setEnabled(bool)
const_iterator constBegin() const const
const_iterator constEnd() const const
bool isEmpty() const const
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addSeparator()
QAction * exec()
virtual bool event(QEvent *e)
void installEventFilter(QObject *filterObj)
void removeEventFilter(QObject *obj)
QString tr(const char *sourceText, const char *disambiguation, int n)
QTextCursor cursorForPosition(const QPoint &pos) const const
void setTextCursor(const QTextCursor &cursor)
QTextCursor textCursor() const const
void chop(qsizetype n)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype length() const const
QString right(qsizetype n) const const
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QString text() const const
QTextBlock block() const const
bool hasSelection() const const
void insertText(const QString &text)
int position() const const
int selectionEnd() const const
int selectionStart() const const
QTextCursor cursorForPosition(const QPoint &pos) const const
void setTextCursor(const QTextCursor &cursor)
QTextCursor textCursor() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:29:33 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.