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

source code of sonnet/src/ui/dialog.cpp