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"), action: m_spellingMenuAction);
73 m_spellingMenu = m_spellingMenuAction->menu();
74 connect(sender: m_spellingMenu, signal: &QMenu::aboutToShow, context: this, slot: &KateSpellingMenu::populateSuggestionsMenu);
75
76 m_ignoreWordAction = new QAction(i18n("Ignore Word"), this);
77 connect(sender: m_ignoreWordAction, signal: &QAction::triggered, context: this, slot: &KateSpellingMenu::ignoreCurrentWord);
78
79 m_addToDictionaryAction = new QAction(i18n("Add to Dictionary"), this);
80 connect(sender: m_addToDictionaryAction, signal: &QAction::triggered, context: this, slot: &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(text: i.key());
87 action->setData(i.value());
88 }
89 connect(sender: m_dictionaryGroup, signal: &QActionGroup::triggered, slot: [this](QAction *action) {
90 if (m_selectedRange.isValid() && !m_selectedRange.isEmpty()) {
91 const bool blockmode = m_view->blockSelection();
92 m_view->doc()->setDictionary(dict: action->data().toString(), range: 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(interval: 0, slot: [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 *>(o: act->parent())->removeAction(action: 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(range: 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(range: *m_currentMisspelledRange);
169 m_spellingMenuAction->setText(i18n("Spelling '%1'", misspelledWord));
170 // Add suggestions on top of menu
171 m_currentDictionary = m_view->doc()->dictionaryForMisspelledRange(range: *m_currentMisspelledRange);
172 m_currentSuggestions = KTextEditor::EditorPrivate::self()->spellCheckManager()->suggestions(word: misspelledWord, dictionary: 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(t: action);
181 connect(sender: action, signal: &QAction::triggered, context: this, slot: [suggestion, this]() {
182 replaceWordBySuggestion(suggestion);
183 });
184 m_spellingMenu->addAction(action);
185 --counter;
186 }
187 contextMenu->insertActions(before: m_spellingMenuAction, actions: 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(action: m_ignoreWordAction);
203 m_spellingMenu->addAction(action: 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(value: m_currentDictionary);
217 QAction *action = m_dictionaryGroup->addAction(text: 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(sender: action, signal: &QAction::triggered, context: this, slot: [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(range: *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(newWord: suggestion, doc, replacementRange: *m_currentMisspelledRange);
256
257 // ...on the replaced word
258 m_view->doc()->setDictionary(dict: dictionary, range: 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(range: *m_currentMisspelledRange);
268 const QString dictionary = m_view->doc()->dictionaryForMisspelledRange(range: *m_currentMisspelledRange);
269 KTextEditor::EditorPrivate::self()->spellCheckManager()->addToDictionary(word: misspelledWord, dictionary);
270 m_view->doc()->clearMisspellingForWord(word: 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(range: *m_currentMisspelledRange);
280 const QString dictionary = m_view->doc()->dictionaryForMisspelledRange(range: *m_currentMisspelledRange);
281 KTextEditor::EditorPrivate::self()->spellCheckManager()->ignoreWord(word: misspelledWord, dictionary);
282 m_view->doc()->clearMisspellingForWord(word: misspelledWord); // WARNING: 'm_currentMisspelledRange' is deleted here!
283 m_view->clearSelection();
284}
285
286#include "moc_spellingmenu.cpp"
287

source code of ktexteditor/src/spellcheck/spellingmenu.cpp