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 | |
33 | class ReadOnlyStringListModel : public QStringListModel |
34 | { |
35 | public: |
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 | */ |
54 | struct 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 | |
79 | class SpellCheckBar::Private |
80 | { |
81 | public: |
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 | |
115 | SpellCheckBar::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 | |
131 | SpellCheckBar::~SpellCheckBar() |
132 | { |
133 | delete d; |
134 | } |
135 | |
136 | void 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 | |
151 | void 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 | |
174 | void 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 | |
199 | void 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 | |
208 | void SpellCheckBar::showProgressDialog(int timeout) |
209 | { |
210 | d->progressDialogTimeout = timeout; |
211 | } |
212 | |
213 | void SpellCheckBar::showSpellCheckCompletionMessage(bool b) |
214 | { |
215 | d->showCompletionMessageBox = b; |
216 | } |
217 | |
218 | void SpellCheckBar::setSpellCheckContinuedAfterReplacement(bool b) |
219 | { |
220 | d->spellCheckContinuedAfterReplacement = b; |
221 | } |
222 | |
223 | void 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 | |
231 | void SpellCheckBar::setGuiEnabled(bool b) |
232 | { |
233 | d->wdg->setEnabled(b); |
234 | } |
235 | |
236 | void 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 | |
260 | void SpellCheckBar::slotCancel() |
261 | { |
262 | hideMe(); |
263 | } |
264 | |
265 | QString SpellCheckBar::originalBuffer() const |
266 | { |
267 | return d->originalBuffer; |
268 | } |
269 | |
270 | QString SpellCheckBar::buffer() const |
271 | { |
272 | return d->checker->text(); |
273 | } |
274 | |
275 | void 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 | |
282 | void 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 | |
293 | void SpellCheckBar::updateDictionaryComboBox() |
294 | { |
295 | Sonnet::Speller speller = d->checker->speller(); |
296 | d->ui.m_language->setCurrentByDictionary(speller.language()); |
297 | } |
298 | |
299 | void 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 | |
313 | void 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 | |
326 | void SpellCheckBar::slotAddWord() |
327 | { |
328 | setGuiEnabled(false); |
329 | setProgressDialogVisible(true); |
330 | d->checker->addWordToPersonal(word: d->currentWord.word); |
331 | d->checker->continueChecking(); |
332 | } |
333 | |
334 | void 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 | |
350 | void 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 | |
358 | void SpellCheckBar::slotSkip() |
359 | { |
360 | setGuiEnabled(false); |
361 | setProgressDialogVisible(true); |
362 | d->checker->continueChecking(); |
363 | } |
364 | |
365 | void 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 | |
376 | void SpellCheckBar::slotSuggest() |
377 | { |
378 | QStringList suggs = d->checker->suggest(word: d->ui.cmbReplacement->lineEdit()->text()); |
379 | fillSuggestions(suggs); |
380 | } |
381 | |
382 | void 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 | |
393 | void SpellCheckBar::fillSuggestions(const QStringList &suggs) |
394 | { |
395 | d->suggestionsModel->setStringList(suggs); |
396 | if (!suggs.isEmpty()) { |
397 | d->ui.cmbReplacement->setCurrentIndex(0); |
398 | } |
399 | } |
400 | |
401 | void 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 | |
422 | void 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 | |