KTextEditor

spellcheckbar.cpp
1 /*
2  SPDX-FileCopyrightText: 2003 Zack Rusin <[email protected]>
3  SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <[email protected]>
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);
44  }
45 };
46 
47 /**
48  * Structure abstracts the word and its position in the
49  * parent text.
50  *
51  * @author Zack Rusin <[email protected]>
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;
89 
90  Word currentWord;
91  QMap<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(this);
140  }
141 
142  // called from hideMe, so don't call it again!
143  d->canceled = true;
144  d->deleteProgressDialog(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(d->ui.m_addBtn, &QPushButton::clicked, this, &SpellCheckBar::slotAddWord);
154  connect(d->ui.m_replaceBtn, &QPushButton::clicked, this, &SpellCheckBar::slotReplaceWord);
155  connect(d->ui.m_replaceAllBtn, &QPushButton::clicked, this, &SpellCheckBar::slotReplaceAll);
156  connect(d->ui.m_skipBtn, &QPushButton::clicked, this, &SpellCheckBar::slotSkip);
157  connect(d->ui.m_skipAllBtn, &QPushButton::clicked, this, &SpellCheckBar::slotSkipAll);
158  connect(d->ui.m_suggestBtn, &QPushButton::clicked, this, &SpellCheckBar::slotSuggest);
159  connect(d->ui.m_language, &Sonnet::DictionaryComboBox::textActivated, this, &SpellCheckBar::slotChangeLanguage);
160  connect(d->checker, &Sonnet::BackgroundChecker::misspelling, this, &SpellCheckBar::slotMisspelling);
161  connect(d->checker, &Sonnet::BackgroundChecker::done, this, &SpellCheckBar::slotDone);
162  /*
163  connect(d->ui.m_suggestions, SIGNAL(doubleClicked(QModelIndex)),
164  SLOT(slotReplaceWord()));
165  */
166 
167  // TODO KF6 remove QOverload usage here, only KComboBox::returnPressed(const QString &) will remain
168  connect(d->ui.cmbReplacement, qOverload<const QString &>(&KComboBox::returnPressed), this, &SpellCheckBar::slotReplaceWord);
169  connect(d->ui.m_autoCorrect, &QPushButton::clicked, this, &SpellCheckBar::slotAutocorrect);
170  // button use by kword/kpresenter
171  // hide by default
172  d->ui.m_autoCorrect->hide();
173 }
174 
175 void SpellCheckBar::initGui()
176 {
177  QVBoxLayout *layout = new QVBoxLayout(centralWidget());
178  layout->setContentsMargins(0, 0, 0, 0);
179 
180  d->wdg = new QWidget(this);
181  d->ui.setupUi(d->wdg);
182  layout->addWidget(d->wdg);
183  setGuiEnabled(false);
184 
185  /*
186  d->buttonBox = new QDialogButtonBox(this);
187  d->buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
188 
189  layout->addWidget(d->buttonBox);
190  */
191 
192  // d->ui.m_suggestions->setSorting( NONSORTINGCOLUMN );
193  fillDictionaryComboBox();
194  d->restart = false;
195 
196  d->suggestionsModel = new ReadOnlyStringListModel(this);
197  d->ui.cmbReplacement->setModel(d->suggestionsModel);
198 }
199 
200 void SpellCheckBar::activeAutoCorrect(bool _active)
201 {
202  if (_active) {
203  d->ui.m_autoCorrect->show();
204  } else {
205  d->ui.m_autoCorrect->hide();
206  }
207 }
208 
210 {
211  d->progressDialogTimeout = timeout;
212 }
213 
215 {
216  d->showCompletionMessageBox = b;
217 }
218 
220 {
221  d->spellCheckContinuedAfterReplacement = b;
222 }
223 
224 void SpellCheckBar::slotAutocorrect()
225 {
226  setGuiEnabled(false);
227  setProgressDialogVisible(true);
228  Q_EMIT autoCorrect(d->currentWord.word, d->ui.cmbReplacement->lineEdit()->text());
229  slotReplaceWord();
230 }
231 
232 void SpellCheckBar::setGuiEnabled(bool b)
233 {
234  d->wdg->setEnabled(b);
235 }
236 
237 void SpellCheckBar::setProgressDialogVisible(bool b)
238 {
239  if (!b) {
240  d->deleteProgressDialog(true);
241  } else if (d->progressDialogTimeout >= 0) {
242  if (d->progressDialog) {
243  return;
244  }
245  d->progressDialog = new QProgressDialog(this);
246  d->progressDialog->setLabelText(i18nc("progress label", "Spell checking in progress..."));
247  d->progressDialog->setWindowTitle(i18nc("@title:window", "Check Spelling"));
248  d->progressDialog->setModal(true);
249  d->progressDialog->setAutoClose(false);
250  d->progressDialog->setAutoReset(false);
251  // create an 'indefinite' progress box as we currently cannot get progress feedback from
252  // the speller
253  d->progressDialog->reset();
254  d->progressDialog->setRange(0, 0);
255  d->progressDialog->setValue(0);
256  connect(d->progressDialog, &QProgressDialog::canceled, this, &SpellCheckBar::slotCancel);
257  d->progressDialog->setMinimumDuration(d->progressDialogTimeout);
258  }
259 }
260 
261 void SpellCheckBar::slotCancel()
262 {
263  hideMe();
264 }
265 
266 QString SpellCheckBar::originalBuffer() const
267 {
268  return d->originalBuffer;
269 }
270 
271 QString SpellCheckBar::buffer() const
272 {
273  return d->checker->text();
274 }
275 
276 void SpellCheckBar::setBuffer(const QString &buf)
277 {
278  d->originalBuffer = buf;
279  // it is possible to change buffer inside slot connected to done() signal
280  d->restart = true;
281 }
282 
283 void SpellCheckBar::fillDictionaryComboBox()
284 {
285  // Since m_language is changed to DictionaryComboBox most code here is gone,
286  // So fillDictionaryComboBox() could be removed and code moved to initGui()
287  // because the call in show() looks obsolete
288  Sonnet::Speller speller = d->checker->speller();
289  d->dictsMap = speller.availableDictionaries();
290 
291  updateDictionaryComboBox();
292 }
293 
294 void SpellCheckBar::updateDictionaryComboBox()
295 {
296  Sonnet::Speller speller = d->checker->speller();
297  d->ui.m_language->setCurrentByDictionary(speller.language());
298 }
299 
300 void SpellCheckBar::updateDialog(const QString &word)
301 {
302  d->ui.m_unknownWord->setText(word);
303  // d->ui.m_contextLabel->setText(d->checker->currentContext());
304  const QStringList suggs = d->checker->suggest(word);
305 
306  if (suggs.isEmpty()) {
307  d->ui.cmbReplacement->lineEdit()->clear();
308  } else {
309  d->ui.cmbReplacement->lineEdit()->setText(suggs.first());
310  }
311  fillSuggestions(suggs);
312 }
313 
314 void SpellCheckBar::show()
315 {
316  d->canceled = false;
317  fillDictionaryComboBox();
318  updateDictionaryComboBox();
319  if (d->originalBuffer.isEmpty()) {
320  d->checker->start();
321  } else {
322  d->checker->setText(d->originalBuffer);
323  }
324  setProgressDialogVisible(true);
325 }
326 
327 void SpellCheckBar::slotAddWord()
328 {
329  setGuiEnabled(false);
330  setProgressDialogVisible(true);
331  d->checker->addWordToPersonal(d->currentWord.word);
332  d->checker->continueChecking();
333 }
334 
335 void SpellCheckBar::slotReplaceWord()
336 {
337  setGuiEnabled(false);
338  setProgressDialogVisible(true);
339  const QString replacementText = d->ui.cmbReplacement->lineEdit()->text();
340  Q_EMIT replace(d->currentWord.word, d->currentWord.start, replacementText);
341 
342  if (d->spellCheckContinuedAfterReplacement) {
343  d->checker->replace(d->currentWord.start, d->currentWord.word, replacementText);
344  d->checker->continueChecking();
345  } else {
346  setProgressDialogVisible(false);
347  d->checker->stop();
348  }
349 }
350 
351 void SpellCheckBar::slotReplaceAll()
352 {
353  setGuiEnabled(false);
354  setProgressDialogVisible(true);
355  d->replaceAllMap.insert(d->currentWord.word, d->ui.cmbReplacement->lineEdit()->text());
356  slotReplaceWord();
357 }
358 
359 void SpellCheckBar::slotSkip()
360 {
361  setGuiEnabled(false);
362  setProgressDialogVisible(true);
363  d->checker->continueChecking();
364 }
365 
366 void SpellCheckBar::slotSkipAll()
367 {
368  setGuiEnabled(false);
369  setProgressDialogVisible(true);
370  //### do we want that or should we have a d->ignoreAll list?
371  Sonnet::Speller speller = d->checker->speller();
372  speller.addToPersonal(d->currentWord.word);
373  d->checker->setSpeller(speller);
374  d->checker->continueChecking();
375 }
376 
377 void SpellCheckBar::slotSuggest()
378 {
379  QStringList suggs = d->checker->suggest(d->ui.cmbReplacement->lineEdit()->text());
380  fillSuggestions(suggs);
381 }
382 
383 void SpellCheckBar::slotChangeLanguage(const QString &lang)
384 {
385  Sonnet::Speller speller = d->checker->speller();
386  QString languageCode = d->dictsMap[lang];
387  if (!languageCode.isEmpty()) {
388  d->checker->changeLanguage(languageCode);
389  slotSuggest();
390  Q_EMIT languageChanged(languageCode);
391  }
392 }
393 
394 void SpellCheckBar::fillSuggestions(const QStringList &suggs)
395 {
396  d->suggestionsModel->setStringList(suggs);
397  if (!suggs.isEmpty()) {
398  d->ui.cmbReplacement->setCurrentIndex(0);
399  }
400 }
401 
402 void SpellCheckBar::slotMisspelling(const QString &word, int start)
403 {
404  setGuiEnabled(true);
405  setProgressDialogVisible(false);
406  Q_EMIT misspelling(word, start);
407  // NOTE this is HACK I had to introduce because BackgroundChecker lacks 'virtual' marks on methods
408  // this dramatically reduces spellchecking time in Lokalize
409  // as this doesn't fetch suggestions for words that are present in msgid
410  if (!updatesEnabled()) {
411  return;
412  }
413 
414  d->currentWord = Word(word, start);
415  if (d->replaceAllMap.contains(word)) {
416  d->ui.cmbReplacement->lineEdit()->setText(d->replaceAllMap[word]);
417  slotReplaceWord();
418  } else {
419  updateDialog(word);
420  }
421 }
422 
423 void SpellCheckBar::slotDone()
424 {
425  d->restart = false;
426  Q_EMIT done(d->checker->text());
427  if (d->restart) {
428  updateDictionaryComboBox();
429  d->checker->setText(d->originalBuffer);
430  d->restart = false;
431  } else {
432  setProgressDialogVisible(false);
433  Q_EMIT spellCheckStatus(i18n("Spell check complete."));
434  hideMe();
435  if (!d->canceled && d->showCompletionMessageBox) {
436  QMessageBox::information(this, i18n("Spell check complete."), i18nc("@title:window", "Check Spelling"));
437  }
438  }
439 }
T & first()
void misspelling(const QString &word, int start)
void clicked(bool checked)
Q_SCRIPTABLE Q_NOREPLY void start()
void textActivated(const QString &text)
void showProgressDialog(int timeout=500)
Controls whether an (indefinite) progress dialog is shown when the spell checking takes longer than t...
void showSpellCheckCompletionMessage(bool b=true)
Controls whether a message box indicating the completion of the spell checking is shown or not.
void hide()
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
KGuiItem cancel()
void deleteLater()
typedef ItemFlags
QString i18n(const char *text, const TYPE &arg...)
bool isEmpty() const const
QAction * replace(const QObject *recvr, const char *slot, QObject *parent)
bool isEmpty() const const
QAction * restart(const QObject *recvr, const char *slot, QObject *parent)
virtual Qt::ItemFlags flags(const QModelIndex &index) const const override
void returnPressed()
void setContentsMargins(int left, int top, int right, int bottom)
bool addToPersonal(const QString &word)
void clear()
QMap< QString, QString > availableDictionaries() const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString language() const
QMessageBox::StandardButton information(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton)
void setSpellCheckContinuedAfterReplacement(bool b)
Controls whether the spell checking is continued after the replacement of a misspelled word has been ...
const QList< QKeySequence > & end()
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Fri Aug 12 2022 03:48:55 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.