1/*
2 SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
3 SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <michel.ludwig@kdemail.net>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "spellcheckbar.h"
9#include "ui_spellcheckbar.h"
10#include <KLocalizedString>
11
12#include "sonnet/backgroundchecker.h"
13#include "sonnet/speller.h"
14/*
15#include "sonnet/filter_p.h"
16#include "sonnet/settings_p.h"
17*/
18
19#include <QProgressDialog>
20
21#include <QComboBox>
22#include <QDialogButtonBox>
23#include <QLabel>
24#include <QListView>
25#include <QMessageBox>
26#include <QPushButton>
27#include <QStringListModel>
28#include <QTimer>
29
30// to initially disable sorting in the suggestions listview
31#define NONSORTINGCOLUMN 2
32
33class ReadOnlyStringListModel : public QStringListModel
34{
35public:
36 ReadOnlyStringListModel(QObject *parent)
37 : QStringListModel(parent)
38 {
39 }
40 Qt::ItemFlags flags(const QModelIndex &index) const override
41 {
42 Q_UNUSED(index);
43 return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
44 }
45};
46
47/**
48 * Structure abstracts the word and its position in the
49 * parent text.
50 *
51 * @author Zack Rusin <zack@kde.org>
52 * @short struct represents word
53 */
54struct Word {
55 Word()
56 {
57 }
58
59 Word(const QString &w, int st, bool e = false)
60 : word(w)
61 , start(st)
62 , end(e)
63 {
64 }
65 Word(const Word &other)
66 : word(other.word)
67 , start(other.start)
68 , end(other.end)
69 {
70 }
71
72 Word &operator=(const Word &) = default;
73
74 QString word;
75 int start = 0;
76 bool end = true;
77};
78
79class SpellCheckBar::Private
80{
81public:
82 Ui_SonnetUi ui;
83 ReadOnlyStringListModel *suggestionsModel;
84 QWidget *wdg;
85 QDialogButtonBox *buttonBox;
86 QProgressDialog *progressDialog;
87 QString originalBuffer;
88 Sonnet::BackgroundChecker *checker;
89
90 Word currentWord;
91 std::map<QString, QString> replaceAllMap;
92 bool restart; // used when text is distributed across several qtextedits, eg in KAider
93
94 QMap<QString, QString> dictsMap;
95
96 int progressDialogTimeout;
97 bool showCompletionMessageBox;
98 bool spellCheckContinuedAfterReplacement;
99 bool canceled;
100
101 void deleteProgressDialog(bool directly)
102 {
103 if (progressDialog) {
104 progressDialog->hide();
105 if (directly) {
106 delete progressDialog;
107 } else {
108 progressDialog->deleteLater();
109 }
110 progressDialog = nullptr;
111 }
112 }
113};
114
115SpellCheckBar::SpellCheckBar(Sonnet::BackgroundChecker *checker, QWidget *parent)
116 : KateViewBarWidget(true, parent)
117 , d(new Private)
118{
119 d->checker = checker;
120
121 d->canceled = false;
122 d->showCompletionMessageBox = false;
123 d->spellCheckContinuedAfterReplacement = true;
124 d->progressDialogTimeout = -1;
125 d->progressDialog = nullptr;
126
127 initGui();
128 initConnections();
129}
130
131SpellCheckBar::~SpellCheckBar()
132{
133 delete d;
134}
135
136void SpellCheckBar::closed()
137{
138 if (viewBar()) {
139 viewBar()->removeBarWidget(barWidget: this);
140 }
141
142 // called from hideMe, so don't call it again!
143 d->canceled = true;
144 d->deleteProgressDialog(directly: false); // this method can be called in response to
145 d->replaceAllMap.clear();
146 // pressing 'Cancel' on the dialog
147 Q_EMIT cancel();
148 Q_EMIT spellCheckStatus(i18n("Spell check canceled."));
149}
150
151void SpellCheckBar::initConnections()
152{
153 connect(sender: d->ui.m_addBtn, signal: &QPushButton::clicked, context: this, slot: &SpellCheckBar::slotAddWord);
154 connect(sender: d->ui.m_replaceBtn, signal: &QPushButton::clicked, context: this, slot: &SpellCheckBar::slotReplaceWord);
155 connect(sender: d->ui.m_replaceAllBtn, signal: &QPushButton::clicked, context: this, slot: &SpellCheckBar::slotReplaceAll);
156 connect(sender: d->ui.m_skipBtn, signal: &QPushButton::clicked, context: this, slot: &SpellCheckBar::slotSkip);
157 connect(sender: d->ui.m_skipAllBtn, signal: &QPushButton::clicked, context: this, slot: &SpellCheckBar::slotSkipAll);
158 connect(sender: d->ui.m_suggestBtn, signal: &QPushButton::clicked, context: this, slot: &SpellCheckBar::slotSuggest);
159 connect(sender: d->ui.m_language, signal: &Sonnet::DictionaryComboBox::textActivated, context: this, slot: &SpellCheckBar::slotChangeLanguage);
160 connect(sender: d->checker, signal: &Sonnet::BackgroundChecker::misspelling, context: this, slot: &SpellCheckBar::slotMisspelling);
161 connect(sender: d->checker, signal: &Sonnet::BackgroundChecker::done, context: this, slot: &SpellCheckBar::slotDone);
162 /*
163 connect(d->ui.m_suggestions, SIGNAL(doubleClicked(QModelIndex)),
164 SLOT(slotReplaceWord()));
165 */
166
167 connect(sender: d->ui.cmbReplacement, signal: &KComboBox::returnPressed, context: this, slot: &SpellCheckBar::slotReplaceWord);
168 connect(sender: d->ui.m_autoCorrect, signal: &QPushButton::clicked, context: this, slot: &SpellCheckBar::slotAutocorrect);
169 // button use by kword/kpresenter
170 // hide by default
171 d->ui.m_autoCorrect->hide();
172}
173
174void SpellCheckBar::initGui()
175{
176 QVBoxLayout *layout = new QVBoxLayout(centralWidget());
177 layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
178
179 d->wdg = new QWidget(this);
180 d->ui.setupUi(d->wdg);
181 layout->addWidget(d->wdg);
182 setGuiEnabled(false);
183
184 /*
185 d->buttonBox = new QDialogButtonBox(this);
186 d->buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
187
188 layout->addWidget(d->buttonBox);
189 */
190
191 // d->ui.m_suggestions->setSorting( NONSORTINGCOLUMN );
192 fillDictionaryComboBox();
193 d->restart = false;
194
195 d->suggestionsModel = new ReadOnlyStringListModel(this);
196 d->ui.cmbReplacement->setModel(d->suggestionsModel);
197}
198
199void SpellCheckBar::activeAutoCorrect(bool _active)
200{
201 if (_active) {
202 d->ui.m_autoCorrect->show();
203 } else {
204 d->ui.m_autoCorrect->hide();
205 }
206}
207
208void SpellCheckBar::showProgressDialog(int timeout)
209{
210 d->progressDialogTimeout = timeout;
211}
212
213void SpellCheckBar::showSpellCheckCompletionMessage(bool b)
214{
215 d->showCompletionMessageBox = b;
216}
217
218void SpellCheckBar::setSpellCheckContinuedAfterReplacement(bool b)
219{
220 d->spellCheckContinuedAfterReplacement = b;
221}
222
223void SpellCheckBar::slotAutocorrect()
224{
225 setGuiEnabled(false);
226 setProgressDialogVisible(true);
227 Q_EMIT autoCorrect(currentWord: d->currentWord.word, replaceWord: d->ui.cmbReplacement->lineEdit()->text());
228 slotReplaceWord();
229}
230
231void SpellCheckBar::setGuiEnabled(bool b)
232{
233 d->wdg->setEnabled(b);
234}
235
236void SpellCheckBar::setProgressDialogVisible(bool b)
237{
238 if (!b) {
239 d->deleteProgressDialog(directly: true);
240 } else if (d->progressDialogTimeout >= 0) {
241 if (d->progressDialog) {
242 return;
243 }
244 d->progressDialog = new QProgressDialog(this);
245 d->progressDialog->setLabelText(i18nc("progress label", "Spell checking in progress..."));
246 d->progressDialog->setWindowTitle(i18nc("@title:window", "Check Spelling"));
247 d->progressDialog->setModal(true);
248 d->progressDialog->setAutoClose(false);
249 d->progressDialog->setAutoReset(false);
250 // create an 'indefinite' progress box as we currently cannot get progress feedback from
251 // the speller
252 d->progressDialog->reset();
253 d->progressDialog->setRange(minimum: 0, maximum: 0);
254 d->progressDialog->setValue(0);
255 connect(sender: d->progressDialog, signal: &QProgressDialog::canceled, context: this, slot: &SpellCheckBar::slotCancel);
256 d->progressDialog->setMinimumDuration(d->progressDialogTimeout);
257 }
258}
259
260void SpellCheckBar::slotCancel()
261{
262 hideMe();
263}
264
265QString SpellCheckBar::originalBuffer() const
266{
267 return d->originalBuffer;
268}
269
270QString SpellCheckBar::buffer() const
271{
272 return d->checker->text();
273}
274
275void SpellCheckBar::setBuffer(const QString &buf)
276{
277 d->originalBuffer = buf;
278 // it is possible to change buffer inside slot connected to done() signal
279 d->restart = true;
280}
281
282void SpellCheckBar::fillDictionaryComboBox()
283{
284 // Since m_language is changed to DictionaryComboBox most code here is gone,
285 // So fillDictionaryComboBox() could be removed and code moved to initGui()
286 // because the call in show() looks obsolete
287 Sonnet::Speller speller = d->checker->speller();
288 d->dictsMap = speller.availableDictionaries();
289
290 updateDictionaryComboBox();
291}
292
293void SpellCheckBar::updateDictionaryComboBox()
294{
295 Sonnet::Speller speller = d->checker->speller();
296 d->ui.m_language->setCurrentByDictionary(speller.language());
297}
298
299void SpellCheckBar::updateDialog(const QString &word)
300{
301 d->ui.m_unknownWord->setText(word);
302 // d->ui.m_contextLabel->setText(d->checker->currentContext());
303 const QStringList suggs = d->checker->suggest(word);
304
305 if (suggs.isEmpty()) {
306 d->ui.cmbReplacement->lineEdit()->clear();
307 } else {
308 d->ui.cmbReplacement->lineEdit()->setText(suggs.first());
309 }
310 fillSuggestions(suggs);
311}
312
313void SpellCheckBar::show()
314{
315 d->canceled = false;
316 fillDictionaryComboBox();
317 updateDictionaryComboBox();
318 if (d->originalBuffer.isEmpty()) {
319 d->checker->start();
320 } else {
321 d->checker->setText(d->originalBuffer);
322 }
323 setProgressDialogVisible(true);
324}
325
326void SpellCheckBar::slotAddWord()
327{
328 setGuiEnabled(false);
329 setProgressDialogVisible(true);
330 d->checker->addWordToPersonal(word: d->currentWord.word);
331 d->checker->continueChecking();
332}
333
334void SpellCheckBar::slotReplaceWord()
335{
336 setGuiEnabled(false);
337 setProgressDialogVisible(true);
338 const QString replacementText = d->ui.cmbReplacement->lineEdit()->text();
339 Q_EMIT replace(oldWord: d->currentWord.word, start: d->currentWord.start, newWord: replacementText);
340
341 if (d->spellCheckContinuedAfterReplacement) {
342 d->checker->replace(start: d->currentWord.start, oldText: d->currentWord.word, newText: replacementText);
343 d->checker->continueChecking();
344 } else {
345 setProgressDialogVisible(false);
346 d->checker->stop();
347 }
348}
349
350void SpellCheckBar::slotReplaceAll()
351{
352 setGuiEnabled(false);
353 setProgressDialogVisible(true);
354 d->replaceAllMap.insert_or_assign(k: d->currentWord.word, obj: d->ui.cmbReplacement->lineEdit()->text());
355 slotReplaceWord();
356}
357
358void SpellCheckBar::slotSkip()
359{
360 setGuiEnabled(false);
361 setProgressDialogVisible(true);
362 d->checker->continueChecking();
363}
364
365void SpellCheckBar::slotSkipAll()
366{
367 setGuiEnabled(false);
368 setProgressDialogVisible(true);
369 // ### do we want that or should we have a d->ignoreAll list?
370 Sonnet::Speller speller = d->checker->speller();
371 speller.addToPersonal(word: d->currentWord.word);
372 d->checker->setSpeller(speller);
373 d->checker->continueChecking();
374}
375
376void SpellCheckBar::slotSuggest()
377{
378 QStringList suggs = d->checker->suggest(word: d->ui.cmbReplacement->lineEdit()->text());
379 fillSuggestions(suggs);
380}
381
382void SpellCheckBar::slotChangeLanguage(const QString &lang)
383{
384 Sonnet::Speller speller = d->checker->speller();
385 QString languageCode = d->dictsMap[lang];
386 if (!languageCode.isEmpty()) {
387 d->checker->changeLanguage(lang: languageCode);
388 slotSuggest();
389 Q_EMIT languageChanged(language: languageCode);
390 }
391}
392
393void SpellCheckBar::fillSuggestions(const QStringList &suggs)
394{
395 d->suggestionsModel->setStringList(suggs);
396 if (!suggs.isEmpty()) {
397 d->ui.cmbReplacement->setCurrentIndex(0);
398 }
399}
400
401void SpellCheckBar::slotMisspelling(const QString &word, int start)
402{
403 setGuiEnabled(true);
404 setProgressDialogVisible(false);
405 Q_EMIT misspelling(word, start);
406 // NOTE this is HACK I had to introduce because BackgroundChecker lacks 'virtual' marks on methods
407 // this dramatically reduces spellchecking time in Lokalize
408 // as this doesn't fetch suggestions for words that are present in msgid
409 if (!updatesEnabled()) {
410 return;
411 }
412
413 d->currentWord = Word(word, start);
414 if (d->replaceAllMap.find(x: word) != d->replaceAllMap.end()) {
415 d->ui.cmbReplacement->lineEdit()->setText(d->replaceAllMap[word]);
416 slotReplaceWord();
417 } else {
418 updateDialog(word);
419 }
420}
421
422void SpellCheckBar::slotDone()
423{
424 d->restart = false;
425 Q_EMIT done(newBuffer: d->checker->text());
426 if (d->restart) {
427 updateDictionaryComboBox();
428 d->checker->setText(d->originalBuffer);
429 d->restart = false;
430 } else {
431 setProgressDialogVisible(false);
432 Q_EMIT spellCheckStatus(i18n("Spell check complete."));
433 hideMe();
434 if (!d->canceled && d->showCompletionMessageBox) {
435 QMessageBox::information(parent: this, i18n("Spell check complete."), i18nc("@title:window", "Check Spelling"));
436 }
437 }
438}
439
440#include "moc_spellcheckbar.cpp"
441

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