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"