File indexing completed on 2024-04-28 15:30:45
0001 /* 0002 SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org> 0003 SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <michel.ludwig@kdemail.net> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "spellcheckbar.h" 0009 #include "ui_spellcheckbar.h" 0010 #include <KLocalizedString> 0011 0012 #include "sonnet/backgroundchecker.h" 0013 #include "sonnet/speller.h" 0014 /* 0015 #include "sonnet/filter_p.h" 0016 #include "sonnet/settings_p.h" 0017 */ 0018 0019 #include <QProgressDialog> 0020 0021 #include <QComboBox> 0022 #include <QDialogButtonBox> 0023 #include <QLabel> 0024 #include <QListView> 0025 #include <QMessageBox> 0026 #include <QPushButton> 0027 #include <QStringListModel> 0028 #include <QTimer> 0029 0030 // to initially disable sorting in the suggestions listview 0031 #define NONSORTINGCOLUMN 2 0032 0033 class ReadOnlyStringListModel : public QStringListModel 0034 { 0035 public: 0036 ReadOnlyStringListModel(QObject *parent) 0037 : QStringListModel(parent) 0038 { 0039 } 0040 Qt::ItemFlags flags(const QModelIndex &index) const override 0041 { 0042 Q_UNUSED(index); 0043 return Qt::ItemIsEnabled | Qt::ItemIsSelectable; 0044 } 0045 }; 0046 0047 /** 0048 * Structure abstracts the word and its position in the 0049 * parent text. 0050 * 0051 * @author Zack Rusin <zack@kde.org> 0052 * @short struct represents word 0053 */ 0054 struct Word { 0055 Word() 0056 { 0057 } 0058 0059 Word(const QString &w, int st, bool e = false) 0060 : word(w) 0061 , start(st) 0062 , end(e) 0063 { 0064 } 0065 Word(const Word &other) 0066 : word(other.word) 0067 , start(other.start) 0068 , end(other.end) 0069 { 0070 } 0071 0072 Word &operator=(const Word &) = default; 0073 0074 QString word; 0075 int start = 0; 0076 bool end = true; 0077 }; 0078 0079 class SpellCheckBar::Private 0080 { 0081 public: 0082 Ui_SonnetUi ui; 0083 ReadOnlyStringListModel *suggestionsModel; 0084 QWidget *wdg; 0085 QDialogButtonBox *buttonBox; 0086 QProgressDialog *progressDialog; 0087 QString originalBuffer; 0088 Sonnet::BackgroundChecker *checker; 0089 0090 Word currentWord; 0091 QMap<QString, QString> replaceAllMap; 0092 bool restart; // used when text is distributed across several qtextedits, eg in KAider 0093 0094 QMap<QString, QString> dictsMap; 0095 0096 int progressDialogTimeout; 0097 bool showCompletionMessageBox; 0098 bool spellCheckContinuedAfterReplacement; 0099 bool canceled; 0100 0101 void deleteProgressDialog(bool directly) 0102 { 0103 if (progressDialog) { 0104 progressDialog->hide(); 0105 if (directly) { 0106 delete progressDialog; 0107 } else { 0108 progressDialog->deleteLater(); 0109 } 0110 progressDialog = nullptr; 0111 } 0112 } 0113 }; 0114 0115 SpellCheckBar::SpellCheckBar(Sonnet::BackgroundChecker *checker, QWidget *parent) 0116 : KateViewBarWidget(true, parent) 0117 , d(new Private) 0118 { 0119 d->checker = checker; 0120 0121 d->canceled = false; 0122 d->showCompletionMessageBox = false; 0123 d->spellCheckContinuedAfterReplacement = true; 0124 d->progressDialogTimeout = -1; 0125 d->progressDialog = nullptr; 0126 0127 initGui(); 0128 initConnections(); 0129 } 0130 0131 SpellCheckBar::~SpellCheckBar() 0132 { 0133 delete d; 0134 } 0135 0136 void SpellCheckBar::closed() 0137 { 0138 if (viewBar()) { 0139 viewBar()->removeBarWidget(this); 0140 } 0141 0142 // called from hideMe, so don't call it again! 0143 d->canceled = true; 0144 d->deleteProgressDialog(false); // this method can be called in response to 0145 d->replaceAllMap.clear(); 0146 // pressing 'Cancel' on the dialog 0147 Q_EMIT cancel(); 0148 Q_EMIT spellCheckStatus(i18n("Spell check canceled.")); 0149 } 0150 0151 void SpellCheckBar::initConnections() 0152 { 0153 connect(d->ui.m_addBtn, &QPushButton::clicked, this, &SpellCheckBar::slotAddWord); 0154 connect(d->ui.m_replaceBtn, &QPushButton::clicked, this, &SpellCheckBar::slotReplaceWord); 0155 connect(d->ui.m_replaceAllBtn, &QPushButton::clicked, this, &SpellCheckBar::slotReplaceAll); 0156 connect(d->ui.m_skipBtn, &QPushButton::clicked, this, &SpellCheckBar::slotSkip); 0157 connect(d->ui.m_skipAllBtn, &QPushButton::clicked, this, &SpellCheckBar::slotSkipAll); 0158 connect(d->ui.m_suggestBtn, &QPushButton::clicked, this, &SpellCheckBar::slotSuggest); 0159 connect(d->ui.m_language, &Sonnet::DictionaryComboBox::textActivated, this, &SpellCheckBar::slotChangeLanguage); 0160 connect(d->checker, &Sonnet::BackgroundChecker::misspelling, this, &SpellCheckBar::slotMisspelling); 0161 connect(d->checker, &Sonnet::BackgroundChecker::done, this, &SpellCheckBar::slotDone); 0162 /* 0163 connect(d->ui.m_suggestions, SIGNAL(doubleClicked(QModelIndex)), 0164 SLOT(slotReplaceWord())); 0165 */ 0166 0167 // TODO KF6 remove QOverload usage here, only KComboBox::returnPressed(const QString &) will remain 0168 connect(d->ui.cmbReplacement, qOverload<const QString &>(&KComboBox::returnPressed), this, &SpellCheckBar::slotReplaceWord); 0169 connect(d->ui.m_autoCorrect, &QPushButton::clicked, this, &SpellCheckBar::slotAutocorrect); 0170 // button use by kword/kpresenter 0171 // hide by default 0172 d->ui.m_autoCorrect->hide(); 0173 } 0174 0175 void SpellCheckBar::initGui() 0176 { 0177 QVBoxLayout *layout = new QVBoxLayout(centralWidget()); 0178 layout->setContentsMargins(0, 0, 0, 0); 0179 0180 d->wdg = new QWidget(this); 0181 d->ui.setupUi(d->wdg); 0182 layout->addWidget(d->wdg); 0183 setGuiEnabled(false); 0184 0185 /* 0186 d->buttonBox = new QDialogButtonBox(this); 0187 d->buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); 0188 0189 layout->addWidget(d->buttonBox); 0190 */ 0191 0192 // d->ui.m_suggestions->setSorting( NONSORTINGCOLUMN ); 0193 fillDictionaryComboBox(); 0194 d->restart = false; 0195 0196 d->suggestionsModel = new ReadOnlyStringListModel(this); 0197 d->ui.cmbReplacement->setModel(d->suggestionsModel); 0198 } 0199 0200 void SpellCheckBar::activeAutoCorrect(bool _active) 0201 { 0202 if (_active) { 0203 d->ui.m_autoCorrect->show(); 0204 } else { 0205 d->ui.m_autoCorrect->hide(); 0206 } 0207 } 0208 0209 void SpellCheckBar::showProgressDialog(int timeout) 0210 { 0211 d->progressDialogTimeout = timeout; 0212 } 0213 0214 void SpellCheckBar::showSpellCheckCompletionMessage(bool b) 0215 { 0216 d->showCompletionMessageBox = b; 0217 } 0218 0219 void SpellCheckBar::setSpellCheckContinuedAfterReplacement(bool b) 0220 { 0221 d->spellCheckContinuedAfterReplacement = b; 0222 } 0223 0224 void SpellCheckBar::slotAutocorrect() 0225 { 0226 setGuiEnabled(false); 0227 setProgressDialogVisible(true); 0228 Q_EMIT autoCorrect(d->currentWord.word, d->ui.cmbReplacement->lineEdit()->text()); 0229 slotReplaceWord(); 0230 } 0231 0232 void SpellCheckBar::setGuiEnabled(bool b) 0233 { 0234 d->wdg->setEnabled(b); 0235 } 0236 0237 void SpellCheckBar::setProgressDialogVisible(bool b) 0238 { 0239 if (!b) { 0240 d->deleteProgressDialog(true); 0241 } else if (d->progressDialogTimeout >= 0) { 0242 if (d->progressDialog) { 0243 return; 0244 } 0245 d->progressDialog = new QProgressDialog(this); 0246 d->progressDialog->setLabelText(i18nc("progress label", "Spell checking in progress...")); 0247 d->progressDialog->setWindowTitle(i18nc("@title:window", "Check Spelling")); 0248 d->progressDialog->setModal(true); 0249 d->progressDialog->setAutoClose(false); 0250 d->progressDialog->setAutoReset(false); 0251 // create an 'indefinite' progress box as we currently cannot get progress feedback from 0252 // the speller 0253 d->progressDialog->reset(); 0254 d->progressDialog->setRange(0, 0); 0255 d->progressDialog->setValue(0); 0256 connect(d->progressDialog, &QProgressDialog::canceled, this, &SpellCheckBar::slotCancel); 0257 d->progressDialog->setMinimumDuration(d->progressDialogTimeout); 0258 } 0259 } 0260 0261 void SpellCheckBar::slotCancel() 0262 { 0263 hideMe(); 0264 } 0265 0266 QString SpellCheckBar::originalBuffer() const 0267 { 0268 return d->originalBuffer; 0269 } 0270 0271 QString SpellCheckBar::buffer() const 0272 { 0273 return d->checker->text(); 0274 } 0275 0276 void SpellCheckBar::setBuffer(const QString &buf) 0277 { 0278 d->originalBuffer = buf; 0279 // it is possible to change buffer inside slot connected to done() signal 0280 d->restart = true; 0281 } 0282 0283 void SpellCheckBar::fillDictionaryComboBox() 0284 { 0285 // Since m_language is changed to DictionaryComboBox most code here is gone, 0286 // So fillDictionaryComboBox() could be removed and code moved to initGui() 0287 // because the call in show() looks obsolete 0288 Sonnet::Speller speller = d->checker->speller(); 0289 d->dictsMap = speller.availableDictionaries(); 0290 0291 updateDictionaryComboBox(); 0292 } 0293 0294 void SpellCheckBar::updateDictionaryComboBox() 0295 { 0296 Sonnet::Speller speller = d->checker->speller(); 0297 d->ui.m_language->setCurrentByDictionary(speller.language()); 0298 } 0299 0300 void SpellCheckBar::updateDialog(const QString &word) 0301 { 0302 d->ui.m_unknownWord->setText(word); 0303 // d->ui.m_contextLabel->setText(d->checker->currentContext()); 0304 const QStringList suggs = d->checker->suggest(word); 0305 0306 if (suggs.isEmpty()) { 0307 d->ui.cmbReplacement->lineEdit()->clear(); 0308 } else { 0309 d->ui.cmbReplacement->lineEdit()->setText(suggs.first()); 0310 } 0311 fillSuggestions(suggs); 0312 } 0313 0314 void SpellCheckBar::show() 0315 { 0316 d->canceled = false; 0317 fillDictionaryComboBox(); 0318 updateDictionaryComboBox(); 0319 if (d->originalBuffer.isEmpty()) { 0320 d->checker->start(); 0321 } else { 0322 d->checker->setText(d->originalBuffer); 0323 } 0324 setProgressDialogVisible(true); 0325 } 0326 0327 void SpellCheckBar::slotAddWord() 0328 { 0329 setGuiEnabled(false); 0330 setProgressDialogVisible(true); 0331 d->checker->addWordToPersonal(d->currentWord.word); 0332 d->checker->continueChecking(); 0333 } 0334 0335 void SpellCheckBar::slotReplaceWord() 0336 { 0337 setGuiEnabled(false); 0338 setProgressDialogVisible(true); 0339 const QString replacementText = d->ui.cmbReplacement->lineEdit()->text(); 0340 Q_EMIT replace(d->currentWord.word, d->currentWord.start, replacementText); 0341 0342 if (d->spellCheckContinuedAfterReplacement) { 0343 d->checker->replace(d->currentWord.start, d->currentWord.word, replacementText); 0344 d->checker->continueChecking(); 0345 } else { 0346 setProgressDialogVisible(false); 0347 d->checker->stop(); 0348 } 0349 } 0350 0351 void SpellCheckBar::slotReplaceAll() 0352 { 0353 setGuiEnabled(false); 0354 setProgressDialogVisible(true); 0355 d->replaceAllMap.insert(d->currentWord.word, d->ui.cmbReplacement->lineEdit()->text()); 0356 slotReplaceWord(); 0357 } 0358 0359 void SpellCheckBar::slotSkip() 0360 { 0361 setGuiEnabled(false); 0362 setProgressDialogVisible(true); 0363 d->checker->continueChecking(); 0364 } 0365 0366 void SpellCheckBar::slotSkipAll() 0367 { 0368 setGuiEnabled(false); 0369 setProgressDialogVisible(true); 0370 //### do we want that or should we have a d->ignoreAll list? 0371 Sonnet::Speller speller = d->checker->speller(); 0372 speller.addToPersonal(d->currentWord.word); 0373 d->checker->setSpeller(speller); 0374 d->checker->continueChecking(); 0375 } 0376 0377 void SpellCheckBar::slotSuggest() 0378 { 0379 QStringList suggs = d->checker->suggest(d->ui.cmbReplacement->lineEdit()->text()); 0380 fillSuggestions(suggs); 0381 } 0382 0383 void SpellCheckBar::slotChangeLanguage(const QString &lang) 0384 { 0385 Sonnet::Speller speller = d->checker->speller(); 0386 QString languageCode = d->dictsMap[lang]; 0387 if (!languageCode.isEmpty()) { 0388 d->checker->changeLanguage(languageCode); 0389 slotSuggest(); 0390 Q_EMIT languageChanged(languageCode); 0391 } 0392 } 0393 0394 void SpellCheckBar::fillSuggestions(const QStringList &suggs) 0395 { 0396 d->suggestionsModel->setStringList(suggs); 0397 if (!suggs.isEmpty()) { 0398 d->ui.cmbReplacement->setCurrentIndex(0); 0399 } 0400 } 0401 0402 void SpellCheckBar::slotMisspelling(const QString &word, int start) 0403 { 0404 setGuiEnabled(true); 0405 setProgressDialogVisible(false); 0406 Q_EMIT misspelling(word, start); 0407 // NOTE this is HACK I had to introduce because BackgroundChecker lacks 'virtual' marks on methods 0408 // this dramatically reduces spellchecking time in Lokalize 0409 // as this doesn't fetch suggestions for words that are present in msgid 0410 if (!updatesEnabled()) { 0411 return; 0412 } 0413 0414 d->currentWord = Word(word, start); 0415 if (d->replaceAllMap.contains(word)) { 0416 d->ui.cmbReplacement->lineEdit()->setText(d->replaceAllMap[word]); 0417 slotReplaceWord(); 0418 } else { 0419 updateDialog(word); 0420 } 0421 } 0422 0423 void SpellCheckBar::slotDone() 0424 { 0425 d->restart = false; 0426 Q_EMIT done(d->checker->text()); 0427 if (d->restart) { 0428 updateDictionaryComboBox(); 0429 d->checker->setText(d->originalBuffer); 0430 d->restart = false; 0431 } else { 0432 setProgressDialogVisible(false); 0433 Q_EMIT spellCheckStatus(i18n("Spell check complete.")); 0434 hideMe(); 0435 if (!d->canceled && d->showCompletionMessageBox) { 0436 QMessageBox::information(this, i18n("Spell check complete."), i18nc("@title:window", "Check Spelling")); 0437 } 0438 } 0439 } 0440 0441 #include "moc_spellcheckbar.cpp"