File indexing completed on 2024-04-21 03:57:41

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     std::map<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     connect(d->ui.cmbReplacement, &KComboBox::returnPressed, this, &SpellCheckBar::slotReplaceWord);
0168     connect(d->ui.m_autoCorrect, &QPushButton::clicked, this, &SpellCheckBar::slotAutocorrect);
0169     // button use by kword/kpresenter
0170     // hide by default
0171     d->ui.m_autoCorrect->hide();
0172 }
0173 
0174 void SpellCheckBar::initGui()
0175 {
0176     QVBoxLayout *layout = new QVBoxLayout(centralWidget());
0177     layout->setContentsMargins(0, 0, 0, 0);
0178 
0179     d->wdg = new QWidget(this);
0180     d->ui.setupUi(d->wdg);
0181     layout->addWidget(d->wdg);
0182     setGuiEnabled(false);
0183 
0184     /*
0185     d->buttonBox = new QDialogButtonBox(this);
0186     d->buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
0187 
0188     layout->addWidget(d->buttonBox);
0189     */
0190 
0191     // d->ui.m_suggestions->setSorting( NONSORTINGCOLUMN );
0192     fillDictionaryComboBox();
0193     d->restart = false;
0194 
0195     d->suggestionsModel = new ReadOnlyStringListModel(this);
0196     d->ui.cmbReplacement->setModel(d->suggestionsModel);
0197 }
0198 
0199 void SpellCheckBar::activeAutoCorrect(bool _active)
0200 {
0201     if (_active) {
0202         d->ui.m_autoCorrect->show();
0203     } else {
0204         d->ui.m_autoCorrect->hide();
0205     }
0206 }
0207 
0208 void SpellCheckBar::showProgressDialog(int timeout)
0209 {
0210     d->progressDialogTimeout = timeout;
0211 }
0212 
0213 void SpellCheckBar::showSpellCheckCompletionMessage(bool b)
0214 {
0215     d->showCompletionMessageBox = b;
0216 }
0217 
0218 void SpellCheckBar::setSpellCheckContinuedAfterReplacement(bool b)
0219 {
0220     d->spellCheckContinuedAfterReplacement = b;
0221 }
0222 
0223 void SpellCheckBar::slotAutocorrect()
0224 {
0225     setGuiEnabled(false);
0226     setProgressDialogVisible(true);
0227     Q_EMIT autoCorrect(d->currentWord.word, d->ui.cmbReplacement->lineEdit()->text());
0228     slotReplaceWord();
0229 }
0230 
0231 void SpellCheckBar::setGuiEnabled(bool b)
0232 {
0233     d->wdg->setEnabled(b);
0234 }
0235 
0236 void SpellCheckBar::setProgressDialogVisible(bool b)
0237 {
0238     if (!b) {
0239         d->deleteProgressDialog(true);
0240     } else if (d->progressDialogTimeout >= 0) {
0241         if (d->progressDialog) {
0242             return;
0243         }
0244         d->progressDialog = new QProgressDialog(this);
0245         d->progressDialog->setLabelText(i18nc("progress label", "Spell checking in progress..."));
0246         d->progressDialog->setWindowTitle(i18nc("@title:window", "Check Spelling"));
0247         d->progressDialog->setModal(true);
0248         d->progressDialog->setAutoClose(false);
0249         d->progressDialog->setAutoReset(false);
0250         // create an 'indefinite' progress box as we currently cannot get progress feedback from
0251         // the speller
0252         d->progressDialog->reset();
0253         d->progressDialog->setRange(0, 0);
0254         d->progressDialog->setValue(0);
0255         connect(d->progressDialog, &QProgressDialog::canceled, this, &SpellCheckBar::slotCancel);
0256         d->progressDialog->setMinimumDuration(d->progressDialogTimeout);
0257     }
0258 }
0259 
0260 void SpellCheckBar::slotCancel()
0261 {
0262     hideMe();
0263 }
0264 
0265 QString SpellCheckBar::originalBuffer() const
0266 {
0267     return d->originalBuffer;
0268 }
0269 
0270 QString SpellCheckBar::buffer() const
0271 {
0272     return d->checker->text();
0273 }
0274 
0275 void SpellCheckBar::setBuffer(const QString &buf)
0276 {
0277     d->originalBuffer = buf;
0278     // it is possible to change buffer inside slot connected to done() signal
0279     d->restart = true;
0280 }
0281 
0282 void SpellCheckBar::fillDictionaryComboBox()
0283 {
0284     // Since m_language is changed to DictionaryComboBox most code here is gone,
0285     // So fillDictionaryComboBox() could be removed and code moved to initGui()
0286     // because the call in show() looks obsolete
0287     Sonnet::Speller speller = d->checker->speller();
0288     d->dictsMap = speller.availableDictionaries();
0289 
0290     updateDictionaryComboBox();
0291 }
0292 
0293 void SpellCheckBar::updateDictionaryComboBox()
0294 {
0295     Sonnet::Speller speller = d->checker->speller();
0296     d->ui.m_language->setCurrentByDictionary(speller.language());
0297 }
0298 
0299 void SpellCheckBar::updateDialog(const QString &word)
0300 {
0301     d->ui.m_unknownWord->setText(word);
0302     // d->ui.m_contextLabel->setText(d->checker->currentContext());
0303     const QStringList suggs = d->checker->suggest(word);
0304 
0305     if (suggs.isEmpty()) {
0306         d->ui.cmbReplacement->lineEdit()->clear();
0307     } else {
0308         d->ui.cmbReplacement->lineEdit()->setText(suggs.first());
0309     }
0310     fillSuggestions(suggs);
0311 }
0312 
0313 void SpellCheckBar::show()
0314 {
0315     d->canceled = false;
0316     fillDictionaryComboBox();
0317     updateDictionaryComboBox();
0318     if (d->originalBuffer.isEmpty()) {
0319         d->checker->start();
0320     } else {
0321         d->checker->setText(d->originalBuffer);
0322     }
0323     setProgressDialogVisible(true);
0324 }
0325 
0326 void SpellCheckBar::slotAddWord()
0327 {
0328     setGuiEnabled(false);
0329     setProgressDialogVisible(true);
0330     d->checker->addWordToPersonal(d->currentWord.word);
0331     d->checker->continueChecking();
0332 }
0333 
0334 void SpellCheckBar::slotReplaceWord()
0335 {
0336     setGuiEnabled(false);
0337     setProgressDialogVisible(true);
0338     const QString replacementText = d->ui.cmbReplacement->lineEdit()->text();
0339     Q_EMIT replace(d->currentWord.word, d->currentWord.start, replacementText);
0340 
0341     if (d->spellCheckContinuedAfterReplacement) {
0342         d->checker->replace(d->currentWord.start, d->currentWord.word, replacementText);
0343         d->checker->continueChecking();
0344     } else {
0345         setProgressDialogVisible(false);
0346         d->checker->stop();
0347     }
0348 }
0349 
0350 void SpellCheckBar::slotReplaceAll()
0351 {
0352     setGuiEnabled(false);
0353     setProgressDialogVisible(true);
0354     d->replaceAllMap.insert_or_assign(d->currentWord.word, d->ui.cmbReplacement->lineEdit()->text());
0355     slotReplaceWord();
0356 }
0357 
0358 void SpellCheckBar::slotSkip()
0359 {
0360     setGuiEnabled(false);
0361     setProgressDialogVisible(true);
0362     d->checker->continueChecking();
0363 }
0364 
0365 void SpellCheckBar::slotSkipAll()
0366 {
0367     setGuiEnabled(false);
0368     setProgressDialogVisible(true);
0369     // ### do we want that or should we have a d->ignoreAll list?
0370     Sonnet::Speller speller = d->checker->speller();
0371     speller.addToPersonal(d->currentWord.word);
0372     d->checker->setSpeller(speller);
0373     d->checker->continueChecking();
0374 }
0375 
0376 void SpellCheckBar::slotSuggest()
0377 {
0378     QStringList suggs = d->checker->suggest(d->ui.cmbReplacement->lineEdit()->text());
0379     fillSuggestions(suggs);
0380 }
0381 
0382 void SpellCheckBar::slotChangeLanguage(const QString &lang)
0383 {
0384     Sonnet::Speller speller = d->checker->speller();
0385     QString languageCode = d->dictsMap[lang];
0386     if (!languageCode.isEmpty()) {
0387         d->checker->changeLanguage(languageCode);
0388         slotSuggest();
0389         Q_EMIT languageChanged(languageCode);
0390     }
0391 }
0392 
0393 void SpellCheckBar::fillSuggestions(const QStringList &suggs)
0394 {
0395     d->suggestionsModel->setStringList(suggs);
0396     if (!suggs.isEmpty()) {
0397         d->ui.cmbReplacement->setCurrentIndex(0);
0398     }
0399 }
0400 
0401 void SpellCheckBar::slotMisspelling(const QString &word, int start)
0402 {
0403     setGuiEnabled(true);
0404     setProgressDialogVisible(false);
0405     Q_EMIT misspelling(word, start);
0406     // NOTE this is HACK I had to introduce because BackgroundChecker lacks 'virtual' marks on methods
0407     // this dramatically reduces spellchecking time in Lokalize
0408     // as this doesn't fetch suggestions for words that are present in msgid
0409     if (!updatesEnabled()) {
0410         return;
0411     }
0412 
0413     d->currentWord = Word(word, start);
0414     if (d->replaceAllMap.find(word) != d->replaceAllMap.end()) {
0415         d->ui.cmbReplacement->lineEdit()->setText(d->replaceAllMap[word]);
0416         slotReplaceWord();
0417     } else {
0418         updateDialog(word);
0419     }
0420 }
0421 
0422 void SpellCheckBar::slotDone()
0423 {
0424     d->restart = false;
0425     Q_EMIT done(d->checker->text());
0426     if (d->restart) {
0427         updateDictionaryComboBox();
0428         d->checker->setText(d->originalBuffer);
0429         d->restart = false;
0430     } else {
0431         setProgressDialogVisible(false);
0432         Q_EMIT spellCheckStatus(i18n("Spell check complete."));
0433         hideMe();
0434         if (!d->canceled && d->showCompletionMessageBox) {
0435             QMessageBox::information(this, i18n("Spell check complete."), i18nc("@title:window", "Check Spelling"));
0436         }
0437     }
0438 }
0439 
0440 #include "moc_spellcheckbar.cpp"