Sonnet

spellcheckdecorator.cpp
1 /*
2  * spellcheckdecorator.h
3  *
4  * SPDX-FileCopyrightText: 2013 Aurélien Gâteau <[email protected]>
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 Q_DECL_HIDDEN SpellCheckDecorator::Private
22 {
23 public:
24  Private(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  Private(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  bool onContextMenuEvent(QContextMenuEvent *event);
47  void execSuggestionMenu(const QPoint &pos, const QString &word, const QTextCursor &cursor);
48  void createDefaultHighlighter();
49 
50  SpellCheckDecorator *const q;
51  QTextEdit *m_textEdit = nullptr;
52  QPlainTextEdit *m_plainTextEdit = nullptr;
53  Highlighter *m_highlighter = nullptr;
54 };
55 
56 bool SpellCheckDecorator::Private::onContextMenuEvent(QContextMenuEvent *event)
57 {
58  if (!m_highlighter) {
59  createDefaultHighlighter();
60  }
61 
62  // Obtain the cursor at the mouse position and the current cursor
63  QTextCursor cursorAtMouse;
64  if (m_textEdit) {
65  cursorAtMouse = m_textEdit->cursorForPosition(event->pos());
66  } else {
67  cursorAtMouse = m_plainTextEdit->cursorForPosition(event->pos());
68  }
69  const int mousePos = cursorAtMouse.position();
70  QTextCursor cursor;
71  if (m_textEdit) {
72  cursor = m_textEdit->textCursor();
73  } else {
74  cursor = m_plainTextEdit->textCursor();
75  }
76 
77  // Check if the user clicked a selected word
78  /* clang-format off */
79  const bool selectedWordClicked = cursor.hasSelection()
80  && mousePos >= cursor.selectionStart()
81  && mousePos <= cursor.selectionEnd();
82  /* clang-format on */
83 
84  // Get the word under the (mouse-)cursor and see if it is misspelled.
85  // Don't include apostrophes at the start/end of the word in the selection.
86  QTextCursor wordSelectCursor(cursorAtMouse);
87  wordSelectCursor.clearSelection();
88  wordSelectCursor.select(QTextCursor::WordUnderCursor);
89  QString selectedWord = wordSelectCursor.selectedText();
90 
91  bool isMouseCursorInsideWord = true;
92  if ((mousePos < wordSelectCursor.selectionStart() || mousePos >= wordSelectCursor.selectionEnd()) //
93  && (selectedWord.length() > 1)) {
94  isMouseCursorInsideWord = false;
95  }
96 
97  // Clear the selection again, we re-select it below (without the apostrophes).
98  wordSelectCursor.setPosition(wordSelectCursor.position() - selectedWord.size());
99  if (selectedWord.startsWith(QLatin1Char('\'')) || selectedWord.startsWith(QLatin1Char('\"'))) {
100  selectedWord = selectedWord.right(selectedWord.size() - 1);
101  wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor);
102  }
103  if (selectedWord.endsWith(QLatin1Char('\'')) || selectedWord.endsWith(QLatin1Char('\"'))) {
104  selectedWord.chop(1);
105  }
106 
107  wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, selectedWord.size());
108 
109  /* clang-format off */
110  const bool wordIsMisspelled = isMouseCursorInsideWord
111  && m_highlighter
112  && m_highlighter->isActive()
113  && !selectedWord.isEmpty()
114  && m_highlighter->isWordMisspelled(selectedWord);
115  /* clang-format on */
116 
117  // If the user clicked a selected word, do nothing.
118  // If the user clicked somewhere else, move the cursor there.
119  // If the user clicked on a misspelled word, select that word.
120  // Same behavior as in OpenOffice Writer.
121  bool checkBlock = q->isSpellCheckingEnabledForBlock(cursorAtMouse.block().text());
122  if (!selectedWordClicked) {
123  if (wordIsMisspelled && checkBlock) {
124  if (m_textEdit) {
125  m_textEdit->setTextCursor(wordSelectCursor);
126  } else {
127  m_plainTextEdit->setTextCursor(wordSelectCursor);
128  }
129  } else {
130  if (m_textEdit) {
131  m_textEdit->setTextCursor(cursorAtMouse);
132  } else {
133  m_plainTextEdit->setTextCursor(cursorAtMouse);
134  }
135  }
136  if (m_textEdit) {
137  cursor = m_textEdit->textCursor();
138  } else {
139  cursor = m_plainTextEdit->textCursor();
140  }
141  }
142 
143  // Use standard context menu for already selected words, correctly spelled
144  // words and words inside quotes.
145  if (!wordIsMisspelled || selectedWordClicked || !checkBlock) {
146  return false;
147  }
148  execSuggestionMenu(event->globalPos(), selectedWord, cursor);
149  return true;
150 }
151 
152 void SpellCheckDecorator::Private::execSuggestionMenu(const QPoint &pos, const QString &selectedWord, const QTextCursor &_cursor)
153 {
154  QTextCursor cursor = _cursor;
155  QMenu menu; // don't use KMenu here we don't want auto management accelerator
156 
157  // Add the suggestions to the menu
158  const QStringList reps = m_highlighter->suggestionsForWord(selectedWord, cursor);
159  if (reps.isEmpty()) {
160  QAction *suggestionsAction = menu.addAction(tr("No suggestions for %1").arg(selectedWord));
161  suggestionsAction->setEnabled(false);
162  } else {
164  for (QStringList::const_iterator it = reps.constBegin(); it != end; ++it) {
165  menu.addAction(*it);
166  }
167  }
168 
169  menu.addSeparator();
170 
171  QAction *ignoreAction = menu.addAction(tr("Ignore"));
172  QAction *addToDictAction = menu.addAction(tr("Add to Dictionary"));
173  // Execute the popup inline
174  const QAction *selectedAction = menu.exec(pos);
175 
176  if (selectedAction) {
177  // Fails when we're in the middle of a compose-key sequence
178  // Q_ASSERT(cursor.selectedText() == selectedWord);
179 
180  if (selectedAction == ignoreAction) {
181  m_highlighter->ignoreWord(selectedWord);
182  m_highlighter->rehighlight();
183  } else if (selectedAction == addToDictAction) {
184  m_highlighter->addWordToDictionary(selectedWord);
185  m_highlighter->rehighlight();
186  }
187  // Other actions can only be one of the suggested words
188  else {
189  const QString replacement = selectedAction->text();
190  Q_ASSERT(reps.contains(replacement));
191  cursor.insertText(replacement);
192  if (m_textEdit) {
193  m_textEdit->setTextCursor(cursor);
194  } else {
195  m_plainTextEdit->setTextCursor(cursor);
196  }
197  }
198  }
199 }
200 
201 void SpellCheckDecorator::Private::createDefaultHighlighter()
202 {
203  if (m_textEdit) {
204  m_highlighter = new Highlighter(m_textEdit);
205  } else {
206  m_highlighter = new Highlighter(m_plainTextEdit);
207  }
208 }
209 
211  : QObject(textEdit)
212  , d(new Private(this, textEdit))
213 {
214 }
215 
217  : QObject(textEdit)
218  , d(new Private(this, textEdit))
219 {
220 }
221 
222 SpellCheckDecorator::~SpellCheckDecorator()
223 {
224  delete d;
225 }
226 
228 {
229  d->m_highlighter = highlighter;
230 }
231 
233 {
234  if (!d->m_highlighter) {
235  d->createDefaultHighlighter();
236  }
237  return d->m_highlighter;
238 }
239 
240 bool SpellCheckDecorator::eventFilter(QObject * /*obj*/, QEvent *event)
241 {
242  if (event->type() == QEvent::ContextMenu) {
243  return d->onContextMenuEvent(static_cast<QContextMenuEvent *>(event));
244  }
245  return false;
246 }
247 
249 {
250  Q_UNUSED(textBlock);
251  if (d->m_textEdit) {
252  return d->m_textEdit->isEnabled();
253  } else {
254  return d->m_plainTextEdit->isEnabled();
255  }
256 }
257 } // namespace
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
int size() const const
int selectionStart() const const
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
The Sonnet Highlighter class, used for drawing pretty red lines in text fields.
Definition: highlighter.h:23
void chop(int n)
QAction * addSeparator()
QString text() const const
QList::const_iterator constBegin() const const
SpellCheckDecorator(QTextEdit *textEdit)
Creates a spell-check decorator.
QTextBlock block() const const
QAction * addAction(const QString &text)
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...
virtual bool event(QEvent *e)
bool isEmpty() const const
int length() const const
bool isEmpty() const const
Highlighter * highlighter() const
Returns the hightlighter used by the decorator.
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
int selectionEnd() const const
void setHighlighter(Highlighter *highlighter)
Set a custom highlighter on the decorator.
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
The sonnet namespace.
QList::const_iterator constEnd() const const
void setEnabled(bool)
QString right(int n) const const
void insertText(const QString &text)
bool hasSelection() const const
const QList< QKeySequence > & end()
int position() const const
QAction * exec()
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sun Sep 25 2022 04:14:52 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.