diff --git a/src/spellcheck/spellcheckbar.cpp b/src/spellcheck/spellcheckbar.cpp index 053595f1..988c7446 100644 --- a/src/spellcheck/spellcheckbar.cpp +++ b/src/spellcheck/spellcheckbar.cpp @@ -1,448 +1,453 @@ /** * Copyright (C) 2003 Zack Rusin * Copyright (C) 2009-2010 Michel Ludwig * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "spellcheckbar.h" #include "ui_spellcheckbar.h" #include #include "sonnet/backgroundchecker.h" #include "sonnet/speller.h" /* #include "sonnet/filter_p.h" #include "sonnet/settings_p.h" */ #include #include #include #include #include #include #include #include #include //to initially disable sorting in the suggestions listview #define NONSORTINGCOLUMN 2 class ReadOnlyStringListModel: public QStringListModel { public: ReadOnlyStringListModel(QObject *parent): QStringListModel(parent) {} Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE { Q_UNUSED(index); return Qt::ItemIsEnabled | Qt::ItemIsSelectable; } }; /** * Structure abstracts the word and its position in the * parent text. * * @author Zack Rusin * @short struct represents word */ struct Word { Word() : start(0), end(true) {} Word(const QString &w, int st, bool e = false) : word(w), start(st), end(e) {} Word(const Word &other) : word(other.word), start(other.start), end(other.end) {} QString word; int start; bool end; }; class SpellCheckBar::Private { public: Ui_SonnetUi ui; ReadOnlyStringListModel *suggestionsModel; QWidget *wdg; QDialogButtonBox *buttonBox; QProgressDialog *progressDialog; QString originalBuffer; Sonnet::BackgroundChecker *checker; Word currentWord; QMap replaceAllMap; bool restart;//used when text is distributed across several qtextedits, eg in KAider QMap dictsMap; int progressDialogTimeout; bool showCompletionMessageBox; bool spellCheckContinuedAfterReplacement; bool canceled; void deleteProgressDialog(bool directly) { if (progressDialog) { progressDialog->hide(); if (directly) { delete progressDialog; } else { progressDialog->deleteLater(); } progressDialog = nullptr; } } }; SpellCheckBar::SpellCheckBar(Sonnet::BackgroundChecker *checker, QWidget *parent) : KateViewBarWidget(true, parent), d(new Private) { d->checker = checker; d->canceled = false; d->showCompletionMessageBox = false; d->spellCheckContinuedAfterReplacement = true; d->progressDialogTimeout = -1; d->progressDialog = nullptr; initGui(); initConnections(); } SpellCheckBar::~SpellCheckBar() { delete d; } void SpellCheckBar::closed() { if (viewBar()) { viewBar()->removeBarWidget(this); } // called from hideMe, so don't call it again! d->canceled = true; d->deleteProgressDialog(false); // this method can be called in response to + d->replaceAllMap.clear(); // pressing 'Cancel' on the dialog emit cancel(); emit spellCheckStatus(i18n("Spell check canceled.")); } void SpellCheckBar::initConnections() { connect(d->ui.m_addBtn, SIGNAL(clicked()), SLOT(slotAddWord())); connect(d->ui.m_replaceBtn, SIGNAL(clicked()), SLOT(slotReplaceWord())); connect(d->ui.m_replaceAllBtn, SIGNAL(clicked()), SLOT(slotReplaceAll())); connect(d->ui.m_skipBtn, SIGNAL(clicked()), SLOT(slotSkip())); connect(d->ui.m_skipAllBtn, SIGNAL(clicked()), SLOT(slotSkipAll())); connect(d->ui.m_suggestBtn, SIGNAL(clicked()), SLOT(slotSuggest())); connect(d->ui.m_language, SIGNAL(activated(QString)), SLOT(slotChangeLanguage(QString))); connect(d->checker, SIGNAL(misspelling(QString,int)), SLOT(slotMisspelling(QString,int))); connect(d->checker, SIGNAL(done()), SLOT(slotDone())); /* connect(d->ui.m_suggestions, SIGNAL(doubleClicked(QModelIndex)), SLOT(slotReplaceWord())); */ connect(d->ui.cmbReplacement, SIGNAL(returnPressed()), this, SLOT(slotReplaceWord())); connect(d->ui.m_autoCorrect, SIGNAL(clicked()), SLOT(slotAutocorrect())); // button use by kword/kpresenter // hide by default d->ui.m_autoCorrect->hide(); } void SpellCheckBar::initGui() { QVBoxLayout *layout = new QVBoxLayout; layout->setMargin(0); centralWidget()->setLayout(layout); d->wdg = new QWidget(this); d->ui.setupUi(d->wdg); layout->addWidget(d->wdg); setGuiEnabled(false); /* d->buttonBox = new QDialogButtonBox(this); d->buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); layout->addWidget(d->buttonBox); */ //d->ui.m_suggestions->setSorting( NONSORTINGCOLUMN ); fillDictionaryComboBox(); d->restart = false; d->suggestionsModel = new ReadOnlyStringListModel(this); d->ui.cmbReplacement->setModel(d->suggestionsModel); } void SpellCheckBar::activeAutoCorrect(bool _active) { if (_active) { d->ui.m_autoCorrect->show(); } else { d->ui.m_autoCorrect->hide(); } } void SpellCheckBar::showProgressDialog(int timeout) { d->progressDialogTimeout = timeout; } void SpellCheckBar::showSpellCheckCompletionMessage(bool b) { d->showCompletionMessageBox = b; } void SpellCheckBar::setSpellCheckContinuedAfterReplacement(bool b) { d->spellCheckContinuedAfterReplacement = b; } void SpellCheckBar::slotAutocorrect() { setGuiEnabled(false); setProgressDialogVisible(true); emit autoCorrect(d->currentWord.word, d->ui.cmbReplacement->lineEdit()->text()); slotReplaceWord(); } void SpellCheckBar::setGuiEnabled(bool b) { d->wdg->setEnabled(b); } void SpellCheckBar::setProgressDialogVisible(bool b) { if (!b) { d->deleteProgressDialog(true); } else if (d->progressDialogTimeout >= 0) { if (d->progressDialog) { return; } d->progressDialog = new QProgressDialog(this); d->progressDialog->setLabelText(i18nc("progress label", "Spell checking in progress...")); d->progressDialog->setWindowTitle(i18nc("@title:window", "Check Spelling")); d->progressDialog->setModal(true); d->progressDialog->setAutoClose(false); d->progressDialog->setAutoReset(false); // create an 'indefinite' progress box as we currently cannot get progress feedback from // the speller d->progressDialog->reset(); d->progressDialog->setRange(0, 0); d->progressDialog->setValue(0); connect(d->progressDialog, SIGNAL(canceled()), this, SLOT(slotCancel())); d->progressDialog->setMinimumDuration(d->progressDialogTimeout); } } void SpellCheckBar::slotCancel() { hideMe(); } QString SpellCheckBar::originalBuffer() const { return d->originalBuffer; } QString SpellCheckBar::buffer() const { return d->checker->text(); } void SpellCheckBar::setBuffer(const QString &buf) { d->originalBuffer = buf; //it is possible to change buffer inside slot connected to done() signal d->restart = true; } void SpellCheckBar::fillDictionaryComboBox() { Sonnet::Speller speller = d->checker->speller(); d->dictsMap = speller.availableDictionaries(); QStringList langs = d->dictsMap.keys(); d->ui.m_language->clear(); d->ui.m_language->addItems(langs); updateDictionaryComboBox(); } void SpellCheckBar::updateDictionaryComboBox() { Sonnet::Speller speller = d->checker->speller(); d->ui.m_language->setCurrentIndex(d->dictsMap.values().indexOf(speller.language())); } void SpellCheckBar::updateDialog(const QString &word) { d->ui.m_unknownWord->setText(word); //d->ui.m_contextLabel->setText(d->checker->currentContext()); const QStringList suggs = d->checker->suggest(word); if (suggs.isEmpty()) { d->ui.cmbReplacement->lineEdit()->clear(); } else { d->ui.cmbReplacement->lineEdit()->setText(suggs.first()); } fillSuggestions(suggs); } void SpellCheckBar::show() { d->canceled = false; fillDictionaryComboBox(); updateDictionaryComboBox(); if (d->originalBuffer.isEmpty()) { d->checker->start(); } else { d->checker->setText(d->originalBuffer); } setProgressDialogVisible(true); } void SpellCheckBar::slotAddWord() { setGuiEnabled(false); setProgressDialogVisible(true); d->checker->addWordToPersonal(d->currentWord.word); d->checker->continueChecking(); } void SpellCheckBar::slotReplaceWord() { setGuiEnabled(false); setProgressDialogVisible(true); const QString replacementText = d->ui.cmbReplacement->lineEdit()->text(); emit replace(d->currentWord.word, d->currentWord.start, replacementText); if (d->spellCheckContinuedAfterReplacement) { d->checker->replace(d->currentWord.start, d->currentWord.word, replacementText); d->checker->continueChecking(); } else { + setProgressDialogVisible(false); d->checker->stop(); } } void SpellCheckBar::slotReplaceAll() { setGuiEnabled(false); setProgressDialogVisible(true); d->replaceAllMap.insert(d->currentWord.word, d->ui.cmbReplacement->lineEdit()->text()); slotReplaceWord(); } void SpellCheckBar::slotSkip() { setGuiEnabled(false); setProgressDialogVisible(true); d->checker->continueChecking(); } void SpellCheckBar::slotSkipAll() { setGuiEnabled(false); setProgressDialogVisible(true); //### do we want that or should we have a d->ignoreAll list? Sonnet::Speller speller = d->checker->speller(); speller.addToPersonal(d->currentWord.word); d->checker->setSpeller(speller); d->checker->continueChecking(); } void SpellCheckBar::slotSuggest() { QStringList suggs = d->checker->suggest(d->ui.cmbReplacement->lineEdit()->text()); fillSuggestions(suggs); } void SpellCheckBar::slotChangeLanguage(const QString &lang) { Sonnet::Speller speller = d->checker->speller(); QString languageCode = d->dictsMap[lang]; if (!languageCode.isEmpty()) { d->checker->changeLanguage(languageCode); slotSuggest(); emit languageChanged(languageCode); } } void SpellCheckBar::fillSuggestions(const QStringList &suggs) { d->suggestionsModel->setStringList(suggs); + if (!suggs.isEmpty()) { + d->ui.cmbReplacement->setCurrentIndex(0); + } } void SpellCheckBar::slotMisspelling(const QString &word, int start) { setGuiEnabled(true); setProgressDialogVisible(false); emit misspelling(word, start); //NOTE this is HACK I had to introduce because BackgroundChecker lacks 'virtual' marks on methods //this dramatically reduces spellchecking time in Lokalize //as this doesn't fetch suggestions for words that are present in msgid if (!updatesEnabled()) { return; } d->currentWord = Word(word, start); if (d->replaceAllMap.contains(word)) { d->ui.cmbReplacement->lineEdit()->setText(d->replaceAllMap[ word ]); slotReplaceWord(); } else { updateDialog(word); } } void SpellCheckBar::slotDone() { d->restart = false; emit done(d->checker->text()); if (d->restart) { updateDictionaryComboBox(); d->checker->setText(d->originalBuffer); d->restart = false; } else { setProgressDialogVisible(false); emit spellCheckStatus(i18n("Spell check complete.")); hideMe(); if (!d->canceled && d->showCompletionMessageBox) { QMessageBox::information(this, i18n("Spell check complete."), i18nc("@title:window", "Check Spelling")); } } } diff --git a/src/spellcheck/spellcheckdialog.cpp b/src/spellcheck/spellcheckdialog.cpp index 9b8da4a2..4bea658f 100644 --- a/src/spellcheck/spellcheckdialog.cpp +++ b/src/spellcheck/spellcheckdialog.cpp @@ -1,323 +1,326 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2009-2010 by Michel Ludwig * Copyright (C) 2008 Mirko Stocker * Copyright (C) 2004-2005 Anders Lund * Copyright (C) 2002 John Firebaugh * Copyright (C) 2001-2004 Christoph Cullmann * Copyright (C) 2001 Joseph Wenninger * Copyright (C) 1999 Jochen Wilhelmy * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "spellcheckdialog.h" #include "katedocument.h" #include "kateglobal.h" #include "kateview.h" #include "spellcheck/spellcheck.h" #include "spellcheck/spellcheckbar.h" #include #include #include #include #include KateSpellCheckDialog::KateSpellCheckDialog(KTextEditor::ViewPrivate *view) : QObject(view) , m_view(view) , m_spellcheckSelection(nullptr) , m_speller(nullptr) , m_backgroundChecker(nullptr) , m_sonnetDialog(nullptr) , m_globalSpellCheckRange(nullptr) , m_spellCheckCancelledByUser(false) { } KateSpellCheckDialog::~KateSpellCheckDialog() { delete m_globalSpellCheckRange; delete m_sonnetDialog; delete m_backgroundChecker; delete m_speller; } void KateSpellCheckDialog::createActions(KActionCollection *ac) { ac->addAction(KStandardAction::Spelling, this, SLOT(spellcheck())); QAction *a = new QAction(i18n("Spelling (from cursor)..."), this); ac->addAction(QStringLiteral("tools_spelling_from_cursor"), a); a->setIcon(QIcon::fromTheme(QStringLiteral("tools-check-spelling"))); a->setWhatsThis(i18n("Check the document's spelling from the cursor and forward")); connect(a, SIGNAL(triggered()), this, SLOT(spellcheckFromCursor())); m_spellcheckSelection = new QAction(i18n("Spellcheck Selection..."), this); ac->addAction(QStringLiteral("tools_spelling_selection"), m_spellcheckSelection); m_spellcheckSelection->setIcon(QIcon::fromTheme(QStringLiteral("tools-check-spelling"))); m_spellcheckSelection->setWhatsThis(i18n("Check spelling of the selected text")); connect(m_spellcheckSelection, SIGNAL(triggered()), this, SLOT(spellcheckSelection())); } void KateSpellCheckDialog::updateActions() { m_spellcheckSelection->setEnabled(m_view->selection()); } void KateSpellCheckDialog::spellcheckFromCursor() { spellcheck(m_view->cursorPosition()); } void KateSpellCheckDialog::spellcheckSelection() { spellcheck(m_view->selectionRange().start(), m_view->selectionRange().end()); } void KateSpellCheckDialog::spellcheck() { spellcheck(KTextEditor::Cursor(0, 0)); } void KateSpellCheckDialog::spellcheck(const KTextEditor::Cursor &from, const KTextEditor::Cursor &to) { KTextEditor::Cursor start = from; KTextEditor::Cursor end = to; if (end.line() == 0 && end.column() == 0) { end = m_view->doc()->documentEnd(); } if (!m_speller) { m_speller = new Sonnet::Speller(); } m_speller->restore(); if (!m_backgroundChecker) { m_backgroundChecker = new Sonnet::BackgroundChecker(*m_speller); } if (!m_sonnetDialog) { m_sonnetDialog = new SpellCheckBar(m_backgroundChecker, m_view); m_sonnetDialog->showProgressDialog(200); m_sonnetDialog->showSpellCheckCompletionMessage(); m_sonnetDialog->setSpellCheckContinuedAfterReplacement(false); connect(m_sonnetDialog, SIGNAL(done(QString)), this, SLOT(installNextSpellCheckRange())); connect(m_sonnetDialog, SIGNAL(replace(QString,int,QString)), this, SLOT(corrected(QString,int,QString))); connect(m_sonnetDialog, SIGNAL(misspelling(QString,int)), this, SLOT(misspelling(QString,int))); connect(m_sonnetDialog, SIGNAL(cancel()), this, SLOT(cancelClicked())); connect(m_sonnetDialog, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(QObject*))); connect(m_sonnetDialog, SIGNAL(languageChanged(QString)), this, SLOT(languageChanged(QString))); } m_view->bottomViewBar()->addBarWidget(m_sonnetDialog); m_userSpellCheckLanguage.clear(); m_previousGivenSpellCheckLanguage.clear(); delete m_globalSpellCheckRange; // we expand to handle the situation when the last word in the range is replace by a new one m_globalSpellCheckRange = m_view->doc()->newMovingRange(KTextEditor::Range(start, end), KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); m_spellCheckCancelledByUser = false; performSpellCheck(*m_globalSpellCheckRange); } KTextEditor::Cursor KateSpellCheckDialog::locatePosition(int pos) { uint remains; while (m_spellLastPos < (uint)pos) { remains = pos - m_spellLastPos; uint l = m_view->doc()->lineLength(m_spellPosCursor.line()) - m_spellPosCursor.column(); if (l > remains) { m_spellPosCursor.setColumn(m_spellPosCursor.column() + remains); m_spellLastPos = pos; } else { m_spellPosCursor.setLine(m_spellPosCursor.line() + 1); m_spellPosCursor.setColumn(0); m_spellLastPos += l + 1; } } return m_spellPosCursor; } void KateSpellCheckDialog::misspelling(const QString &word, int pos) { KTextEditor::Cursor cursor; int length; int origPos = m_view->doc()->computePositionWrtOffsets(m_currentDecToEncOffsetList, pos); cursor = locatePosition(origPos); length = m_view->doc()->computePositionWrtOffsets(m_currentDecToEncOffsetList, pos + word.length()) - origPos; m_view->setCursorPositionInternal(cursor, 1); m_view->setSelection(KTextEditor::Range(cursor, length)); } void KateSpellCheckDialog::corrected(const QString &word, int pos, const QString &newWord) { int origPos = m_view->doc()->computePositionWrtOffsets(m_currentDecToEncOffsetList, pos); int length = m_view->doc()->computePositionWrtOffsets(m_currentDecToEncOffsetList, pos + word.length()) - origPos; KTextEditor::Cursor replacementStartCursor = locatePosition(origPos); KTextEditor::Range replacementRange = KTextEditor::Range(replacementStartCursor, length); KTextEditor::DocumentPrivate *doc = m_view->doc(); KTextEditor::EditorPrivate::self()->spellCheckManager()->replaceCharactersEncodedIfNecessary(newWord, doc, replacementRange); - m_currentSpellCheckRange.setRange(KTextEditor::Range(replacementStartCursor, m_currentSpellCheckRange.end())); // we have to be careful here: due to static word wrapping the text might change in addition to simply // the misspelled word being replaced, i.e. new line breaks might be inserted as well. As such, the text // in the 'Sonnet::Dialog' might be eventually out of sync with the visible text. Therefore, we 'restart' // spell checking from the current position. performSpellCheck(KTextEditor::Range(replacementStartCursor, m_globalSpellCheckRange->end())); } void KateSpellCheckDialog::performSpellCheck(const KTextEditor::Range &range) { if (range.isEmpty()) { spellCheckDone(); + m_sonnetDialog->closed(); + return; } m_languagesInSpellCheckRange = KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckLanguageRanges(m_view->doc(), range); m_currentLanguageRangeIterator = m_languagesInSpellCheckRange.begin(); m_currentSpellCheckRange = KTextEditor::Range::invalid(); installNextSpellCheckRange(); // first check if there is really something to spell check if (m_currentSpellCheckRange.isValid()) { m_view->bottomViewBar()->showBarWidget(m_sonnetDialog); m_sonnetDialog->show(); m_sonnetDialog->setFocus(); + } else { + m_sonnetDialog->closed(); } } void KateSpellCheckDialog::installNextSpellCheckRange() { if (m_spellCheckCancelledByUser || m_currentLanguageRangeIterator == m_languagesInSpellCheckRange.end()) { spellCheckDone(); return; } KateSpellCheckManager *spellCheckManager = KTextEditor::EditorPrivate::self()->spellCheckManager(); KTextEditor::Cursor nextRangeBegin = (m_currentSpellCheckRange.isValid() ? m_currentSpellCheckRange.end() : KTextEditor::Cursor::invalid()); m_currentSpellCheckRange = KTextEditor::Range::invalid(); m_currentDecToEncOffsetList.clear(); QList > rangeDictionaryPairList; while (m_currentLanguageRangeIterator != m_languagesInSpellCheckRange.end()) { const KTextEditor::Range ¤tLanguageRange = (*m_currentLanguageRangeIterator).first; const QString &dictionary = (*m_currentLanguageRangeIterator).second; KTextEditor::Range languageSubRange = (nextRangeBegin.isValid() ? KTextEditor::Range(nextRangeBegin, currentLanguageRange.end()) : currentLanguageRange); rangeDictionaryPairList = spellCheckManager->spellCheckWrtHighlightingRanges(m_view->doc(), languageSubRange, dictionary, false, true); Q_ASSERT(rangeDictionaryPairList.size() <= 1); if (rangeDictionaryPairList.size() == 0) { ++m_currentLanguageRangeIterator; if (m_currentLanguageRangeIterator != m_languagesInSpellCheckRange.end()) { nextRangeBegin = (*m_currentLanguageRangeIterator).first.start(); } } else { m_currentSpellCheckRange = rangeDictionaryPairList.first().first; QString dictionary = rangeDictionaryPairList.first().second; const bool languageChanged = (dictionary != m_previousGivenSpellCheckLanguage); m_previousGivenSpellCheckLanguage = dictionary; // if there was no change of dictionary stemming from the document language ranges and // the user has set a dictionary in the dialog, we use that one if (!languageChanged && !m_userSpellCheckLanguage.isEmpty()) { dictionary = m_userSpellCheckLanguage; } // we only allow the user to override the preset dictionary within a language range // given by the document else if (languageChanged) { m_userSpellCheckLanguage.clear(); } m_spellPosCursor = m_currentSpellCheckRange.start(); m_spellLastPos = 0; m_currentDecToEncOffsetList.clear(); KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList; QString text = m_view->doc()->decodeCharacters(m_currentSpellCheckRange, m_currentDecToEncOffsetList, encToDecOffsetList); // ensure that no empty string is passed on to Sonnet as this can lead to a crash // (bug 228789) if (text.isEmpty()) { nextRangeBegin = m_currentSpellCheckRange.end(); continue; } if (m_speller->language() != dictionary) { m_speller->setLanguage(dictionary); m_backgroundChecker->setSpeller(*m_speller); } m_sonnetDialog->setBuffer(text); break; } } if (m_currentLanguageRangeIterator == m_languagesInSpellCheckRange.end()) { spellCheckDone(); return; } } void KateSpellCheckDialog::cancelClicked() { m_spellCheckCancelledByUser = true; spellCheckDone(); } void KateSpellCheckDialog::spellCheckDone() { m_currentSpellCheckRange = KTextEditor::Range::invalid(); m_currentDecToEncOffsetList.clear(); m_view->clearSelection(); } void KateSpellCheckDialog::objectDestroyed(QObject *object) { Q_UNUSED(object); m_sonnetDialog = nullptr; } void KateSpellCheckDialog::languageChanged(const QString &language) { m_userSpellCheckLanguage = language; } //END