KTextEditor

spellingmenu.cpp
1/*
2 SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <michel.ludwig@kdemail.net>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "spellingmenu.h"
8
9#include "katedocument.h"
10#include "kateglobal.h"
11#include "kateview.h"
12#include "ontheflycheck.h"
13#include "spellcheck/spellcheck.h"
14
15#include "katepartdebug.h"
16
17#include <QActionGroup>
18#include <QMenu>
19
20#include <KLocalizedString>
21#include <KTextEditor/Range>
22
23KateSpellingMenu::KateSpellingMenu(KTextEditor::ViewPrivate *view)
24 : QObject(view)
25 , m_view(view)
26 , m_spellingMenuAction(nullptr)
27 , m_ignoreWordAction(nullptr)
28 , m_addToDictionaryAction(nullptr)
29 , m_spellingMenu(nullptr)
30 , m_currentMisspelledRange(nullptr)
31{
32}
33
34KateSpellingMenu::~KateSpellingMenu()
35{
36 m_currentMisspelledRange = nullptr; // it shouldn't be accessed anymore as it could
37}
38
39bool KateSpellingMenu::isEnabled() const
40{
41 if (!m_spellingMenuAction) {
42 return false;
43 }
44 return m_spellingMenuAction->isEnabled();
45}
46
47bool KateSpellingMenu::isVisible() const
48{
49 if (!m_spellingMenuAction) {
50 return false;
51 }
52 return m_spellingMenuAction->isVisible();
53}
54
55void KateSpellingMenu::setEnabled(bool b)
56{
57 if (m_spellingMenuAction) {
58 m_spellingMenuAction->setEnabled(b);
59 }
60}
61
62void KateSpellingMenu::setVisible(bool b)
63{
64 if (m_spellingMenuAction) {
65 m_spellingMenuAction->setVisible(b);
66 }
67}
68
69void KateSpellingMenu::createActions(KActionCollection *ac)
70{
71 m_spellingMenuAction = new KActionMenu(i18n("Spelling"), this);
72 ac->addAction(QStringLiteral("spelling_suggestions"), m_spellingMenuAction);
73 m_spellingMenu = m_spellingMenuAction->menu();
74 connect(m_spellingMenu, &QMenu::aboutToShow, this, &KateSpellingMenu::populateSuggestionsMenu);
75
76 m_ignoreWordAction = new QAction(i18n("Ignore Word"), this);
77 connect(m_ignoreWordAction, &QAction::triggered, this, &KateSpellingMenu::ignoreCurrentWord);
78
79 m_addToDictionaryAction = new QAction(i18n("Add to Dictionary"), this);
80 connect(m_addToDictionaryAction, &QAction::triggered, this, &KateSpellingMenu::addCurrentWordToDictionary);
81
82 m_dictionaryGroup = new QActionGroup(this);
83 QMapIterator<QString, QString> i(Sonnet::Speller().preferredDictionaries());
84 while (i.hasNext()) {
85 i.next();
86 QAction *action = m_dictionaryGroup->addAction(i.key());
87 action->setData(i.value());
88 }
89 connect(m_dictionaryGroup, &QActionGroup::triggered, [this](QAction *action) {
90 if (m_selectedRange.isValid() && !m_selectedRange.isEmpty()) {
91 const bool blockmode = m_view->blockSelection();
92 m_view->doc()->setDictionary(action->data().toString(), m_selectedRange, blockmode);
93 }
94 });
95
96 setVisible(false);
97}
98
99void KateSpellingMenu::caretEnteredMisspelledRange(KTextEditor::MovingRange *range)
100{
101 if (m_currentMisspelledRange == range) {
102 return;
103 }
104 m_currentMisspelledRange = range;
105}
106
107void KateSpellingMenu::caretExitedMisspelledRange(KTextEditor::MovingRange *range)
108{
109 if (range != m_currentMisspelledRange) {
110 // The order of 'exited' and 'entered' signals was wrong
111 return;
112 }
113 m_currentMisspelledRange = nullptr;
114}
115
116void KateSpellingMenu::rangeDeleted(KTextEditor::MovingRange *range)
117{
118 if (m_currentMisspelledRange == range) {
119 m_currentMisspelledRange = nullptr;
120 }
121}
122
123void KateSpellingMenu::cleanUpAfterShown()
124{
125 // Ugly hack to avoid segfaults.
126 // cleanUpAfterShown/ViewPrivate::aboutToHideContextMenu is called before
127 // some action slot is processed.
128 QTimer::singleShot(0, [this]() {
129 if (m_currentMisspelledRangeNeedCleanUp) {
130 m_currentMisspelledRange = nullptr;
131 m_currentMisspelledRangeNeedCleanUp = false;
132 }
133
134 // We need to remove our list or they will accumulated on next show event
135 for (auto act : m_menuOnTopSuggestionList) {
136 qobject_cast<QWidget *>(act->parent())->removeAction(act);
137 delete act;
138 }
139 m_menuOnTopSuggestionList.clear();
140 });
141}
142
143void KateSpellingMenu::prepareToBeShown(QMenu *contextMenu)
144{
145 Q_ASSERT(contextMenu);
146
147 if (!m_view->doc()->onTheFlySpellChecker()) {
148 // Nothing todo!
149 return;
150 }
151
152 m_selectedRange = m_view->selectionRange();
153 if (m_selectedRange.isValid() && !m_selectedRange.isEmpty()) {
154 // Selected words need a special handling to work properly
155 auto imv = m_view->doc()->onTheFlySpellChecker()->installedMovingRanges(m_selectedRange);
156 for (int i = 0; i < imv.size(); ++i) {
157 if (imv.at(i)->toRange() == m_selectedRange) {
158 m_currentMisspelledRange = imv.at(i);
159 m_currentMisspelledRangeNeedCleanUp = true;
160 break;
161 }
162 }
163 }
164
165 if (m_currentMisspelledRange != nullptr) {
166 setVisible(true);
167 m_selectedRange = m_currentMisspelledRange->toRange(); // Support actions of m_dictionaryGroup
168 const QString &misspelledWord = m_view->doc()->text(*m_currentMisspelledRange);
169 m_spellingMenuAction->setText(i18n("Spelling '%1'", misspelledWord));
170 // Add suggestions on top of menu
171 m_currentDictionary = m_view->doc()->dictionaryForMisspelledRange(*m_currentMisspelledRange);
172 m_currentSuggestions = KTextEditor::EditorPrivate::self()->spellCheckManager()->suggestions(misspelledWord, m_currentDictionary);
173 int counter = 5;
174 QFont boldFont; // Emphasize on-top suggestions, so does Falkon
175 boldFont.setBold(true);
176 for (QStringList::const_iterator i = m_currentSuggestions.cbegin(); i != m_currentSuggestions.cend() && counter > 0; ++i) {
177 const QString &suggestion = *i;
178 QAction *action = new QAction(suggestion, contextMenu);
179 action->setFont(boldFont);
180 m_menuOnTopSuggestionList.append(action);
181 connect(action, &QAction::triggered, this, [suggestion, this]() {
182 replaceWordBySuggestion(suggestion);
183 });
184 m_spellingMenu->addAction(action);
185 --counter;
186 }
187 contextMenu->insertActions(m_spellingMenuAction, m_menuOnTopSuggestionList);
188
189 } else if (m_selectedRange.isValid() && !m_selectedRange.isEmpty()) {
190 setVisible(true);
191 m_spellingMenuAction->setText(i18n("Spelling"));
192 } else {
193 setVisible(false);
194 }
195}
196
197void KateSpellingMenu::populateSuggestionsMenu()
198{
199 m_spellingMenu->clear();
200
201 if (m_currentMisspelledRange) {
202 m_spellingMenu->addAction(m_ignoreWordAction);
203 m_spellingMenu->addAction(m_addToDictionaryAction);
204
205 m_spellingMenu->addSeparator();
206 bool dictFound = false;
207 for (auto action : m_dictionaryGroup->actions()) {
208 action->setCheckable(true);
209 if (action->data().toString() == m_currentDictionary) {
210 dictFound = true;
211 action->setChecked(true);
212 }
213 m_spellingMenu->addAction(action);
214 }
215 if (!dictFound && !m_currentDictionary.isEmpty()) {
216 const QString dictName = Sonnet::Speller().availableDictionaries().key(m_currentDictionary);
217 QAction *action = m_dictionaryGroup->addAction(dictName);
218 action->setData(m_currentDictionary);
219 action->setCheckable(true);
220 action->setChecked(true);
221 m_spellingMenu->addAction(action);
222 }
223
224 m_spellingMenu->addSeparator();
225 int counter = 10;
226 for (QStringList::const_iterator i = m_currentSuggestions.cbegin(); i != m_currentSuggestions.cend() && counter > 0; ++i) {
227 const QString &suggestion = *i;
228 QAction *action = new QAction(suggestion, m_spellingMenu);
229 connect(action, &QAction::triggered, this, [suggestion, this]() {
230 replaceWordBySuggestion(suggestion);
231 });
232 m_spellingMenu->addAction(action);
233 --counter;
234 }
235
236 } else if (m_selectedRange.isValid() && !m_selectedRange.isEmpty()) {
237 for (auto action : m_dictionaryGroup->actions()) {
238 action->setCheckable(false);
239 m_spellingMenu->addAction(action);
240 }
241 }
242}
243
244void KateSpellingMenu::replaceWordBySuggestion(const QString &suggestion)
245{
246 if (!m_currentMisspelledRange) {
247 return;
248 }
249 // Ensure we keep some special dictionary setting...
250 const QString dictionary = m_view->doc()->dictionaryForMisspelledRange(*m_currentMisspelledRange);
251 KTextEditor::Range newRange = m_currentMisspelledRange->toRange();
252 newRange.setEnd(KTextEditor::Cursor(newRange.start().line(), newRange.start().column() + suggestion.size()));
253
254 KTextEditor::DocumentPrivate *doc = m_view->doc();
255 KTextEditor::EditorPrivate::self()->spellCheckManager()->replaceCharactersEncodedIfNecessary(suggestion, doc, *m_currentMisspelledRange);
256
257 // ...on the replaced word
258 m_view->doc()->setDictionary(dictionary, newRange);
259 m_view->clearSelection(); // Ensure cursor move and next right click works properly if there was a selection
260}
261
262void KateSpellingMenu::addCurrentWordToDictionary()
263{
264 if (!m_currentMisspelledRange) {
265 return;
266 }
267 const QString &misspelledWord = m_view->doc()->text(*m_currentMisspelledRange);
268 const QString dictionary = m_view->doc()->dictionaryForMisspelledRange(*m_currentMisspelledRange);
269 KTextEditor::EditorPrivate::self()->spellCheckManager()->addToDictionary(misspelledWord, dictionary);
270 m_view->doc()->clearMisspellingForWord(misspelledWord); // WARNING: 'm_currentMisspelledRange' is deleted here!
271 m_view->clearSelection();
272}
273
274void KateSpellingMenu::ignoreCurrentWord()
275{
276 if (!m_currentMisspelledRange) {
277 return;
278 }
279 const QString &misspelledWord = m_view->doc()->text(*m_currentMisspelledRange);
280 const QString dictionary = m_view->doc()->dictionaryForMisspelledRange(*m_currentMisspelledRange);
281 KTextEditor::EditorPrivate::self()->spellCheckManager()->ignoreWord(misspelledWord, dictionary);
282 m_view->doc()->clearMisspellingForWord(misspelledWord); // WARNING: 'm_currentMisspelledRange' is deleted here!
283 m_view->clearSelection();
284}
285
286#include "moc_spellingmenu.cpp"
QAction * addAction(const QString &name, const QObject *receiver=nullptr, const char *member=nullptr)
The Cursor represents a position in a Document.
Definition cursor.h:75
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
Definition cursor.h:192
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
KateSpellCheckManager * spellCheckManager()
spell check manager
Definition kateglobal.h:304
static KTextEditor::EditorPrivate * self()
Kate Part Internal stuff ;)
A range that is bound to a specific Document, and maintains its position.
const Range toRange() const
Convert this clever range into a dumb one.
An object representing a section of text, from one Cursor to another.
constexpr Cursor start() const noexcept
Get the start position of this range.
void setEnd(Cursor end) noexcept
Set the end cursor to end.
constexpr bool isEmpty() const noexcept
Returns true if this range contains no characters, ie.
constexpr bool isValid() const noexcept
Validity check.
QMap< QString, QString > availableDictionaries() const
QString i18n(const char *text, const TYPE &arg...)
void setCheckable(bool)
void setChecked(bool)
QVariant data() const const
bool isEnabled() const const
void setFont(const QFont &font)
QMenu * menu() const const
void setData(const QVariant &userData)
void setText(const QString &text)
void triggered(bool checked)
bool isVisible() const const
QAction * addAction(QAction *action)
void triggered(QAction *action)
void setBold(bool enable)
void append(const T &value)
QList::const_iterator cbegin() const const
QList::const_iterator cend() const const
void clear()
const Key key(const T &value, const Key &defaultKey) const const
void aboutToShow()
QAction * addAction(const QString &text)
QAction * addSeparator()
void clear()
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
bool isEmpty() const const
int size() const const
QString toString() const const
void insertActions(QAction *before, QList< QAction * > actions)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Feb 24 2024 20:00:58 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.