X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2Ffrontends%2Fqt4%2FGuiSpellchecker.cpp;h=daff427e85e51096ba85cf9daba6416dbf7f40e0;hb=42015a8ebd17092c9b55c9519015119d04f52218;hp=52302e418b5df6f6ea3fbc8018a52eca30124baf;hpb=a9f4ddae2d99d49e496eb968ab9d6b87ed44e83b;p=lyx.git diff --git a/src/frontends/qt4/GuiSpellchecker.cpp b/src/frontends/qt4/GuiSpellchecker.cpp index 52302e418b..daff427e85 100644 --- a/src/frontends/qt4/GuiSpellchecker.cpp +++ b/src/frontends/qt4/GuiSpellchecker.cpp @@ -4,6 +4,7 @@ * Licence details can be found in the file COPYING. * * \author John Levon + * \author Edwin Leuven * * Full author contact details are available in file CREDITS. */ @@ -12,9 +13,23 @@ #include "GuiSpellchecker.h" -#include "ControlSpellchecker.h" #include "qt_helpers.h" +#include "Buffer.h" +#include "BufferParams.h" +#include "BufferView.h" +#include "Cursor.h" +#include "CutAndPaste.h" +#include "debug.h" +#include "gettext.h" +#include "Language.h" +#include "LyXRC.h" +#include "Paragraph.h" + +#include "support/textutils.h" +#include "support/docstring.h" +#include "support/lstrings.h" + #include #include #include @@ -25,33 +40,49 @@ #include #include +#if defined(USE_ASPELL) +# include "ASpell_local.h" +#elif defined(USE_PSPELL) +# include "PSpell.h" +#endif + +#if defined(USE_ISPELL) +# include "ISpell.h" +#else +# include "SpellBase.h" +#endif +#include "frontends/alert.h" + +using std::advance; +using std::distance; +using std::endl; using std::string; + namespace lyx { namespace frontend { -GuiSpellcheckerDialog::GuiSpellcheckerDialog(LyXView & lv) - : GuiDialog(lv, "spellchecker") +using support::bformat; +using support::contains; + +GuiSpellchecker::GuiSpellchecker(GuiView & lv) + : GuiDialog(lv, "spellchecker"), exitEarly_(false), + oldval_(0), newvalue_(0), count_(0), speller_(0) { setupUi(this); setViewTitle(_("Spellchecker")); - setController(new ControlSpellchecker(*this)); connect(closePB, SIGNAL(clicked()), this, SLOT(slotClose())); + connect(replacePB, SIGNAL(clicked()), this, SLOT(replace())); + connect(ignorePB, SIGNAL(clicked()), this, SLOT(ignore())); + connect(replacePB_3, SIGNAL(clicked()), this, SLOT(accept())); + connect(addPB, SIGNAL(clicked()), this, SLOT(add())); - connect(replaceCO, SIGNAL(highlighted(const QString &)), - this, SLOT(replaceChanged(const QString &))); - connect(replacePB, SIGNAL(clicked()), - this, SLOT(replaceClicked())); - connect(ignorePB, SIGNAL(clicked()), - this, SLOT(ignoreClicked())); - connect(replacePB_3, SIGNAL(clicked()), - this, SLOT(acceptClicked())); - connect(addPB, SIGNAL(clicked()), - this, SLOT(addClicked())); + connect(replaceCO, SIGNAL(highlighted(QString)), + this, SLOT(replaceChanged(QString))); connect(suggestionsLW, SIGNAL(itemDoubleClicked(QListWidgetItem*)), - this, SLOT(replaceClicked() ) ); + this, SLOT(replace())); connect(suggestionsLW, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(suggestionChanged(QListWidgetItem*))); @@ -62,37 +93,13 @@ GuiSpellcheckerDialog::GuiSpellcheckerDialog(LyXView & lv) } -ControlSpellchecker & GuiSpellcheckerDialog::controller() -{ - return static_cast(GuiDialog::controller()); -} - - -void GuiSpellcheckerDialog::acceptClicked() -{ - accept(); -} - - -void GuiSpellcheckerDialog::addClicked() +GuiSpellchecker::~GuiSpellchecker() { - add(); + delete speller_; } -void GuiSpellcheckerDialog::replaceClicked() -{ - replace(); -} - - -void GuiSpellcheckerDialog::ignoreClicked() -{ - ignore(); -} - - -void GuiSpellcheckerDialog::suggestionChanged(QListWidgetItem * item) +void GuiSpellchecker::suggestionChanged(QListWidgetItem * item) { if (replaceCO->count() != 0) replaceCO->setItemText(0, item->text()); @@ -103,12 +110,12 @@ void GuiSpellcheckerDialog::suggestionChanged(QListWidgetItem * item) } -void GuiSpellcheckerDialog::replaceChanged(const QString & str) +void GuiSpellchecker::replaceChanged(const QString & str) { if (suggestionsLW->currentItem()->text() == str) return; - for (int i = 0; i < suggestionsLW->count(); ++i) { + for (int i = 0; i != suggestionsLW->count(); ++i) { if (suggestionsLW->item(i)->text() == str) { suggestionsLW->setCurrentRow(i); break; @@ -117,64 +124,87 @@ void GuiSpellcheckerDialog::replaceChanged(const QString & str) } -void GuiSpellcheckerDialog::closeEvent(QCloseEvent * e) +void GuiSpellchecker::closeEvent(QCloseEvent * e) { slotClose(); GuiDialog::closeEvent(e); } -void GuiSpellcheckerDialog::reject() +void GuiSpellchecker::reject() { slotClose(); QDialog::reject(); } -void GuiSpellcheckerDialog::updateContents() +void GuiSpellchecker::updateContents() { - if (isVisibleView() || controller().exitEarly()) - controller().check(); + // The clauses below are needed because the spellchecker + // has many flaws (see bugs 1950, 2218). + // Basically, we have to distinguish the case where a + // spellcheck has already been performed for the whole + // document (exitEarly() == true, isVisible() == false) + // from the rest (exitEarly() == false, isVisible() == true). + // FIXME: rewrite the whole beast! + static bool check_after_early_exit; + if (exitEarly()) { + // a spellcheck has already been performed, + check(); + check_after_early_exit = true; + } + else if (isVisible()) { + // the above check triggers a second update, + // and isVisible() is true then. Prevent a + // second check that skips the first word + if (check_after_early_exit) + // don't check, but reset the bool. + // business as usual after this. + check_after_early_exit = false; + else + // perform spellcheck (default case) + check(); + } } -void GuiSpellcheckerDialog::accept() +void GuiSpellchecker::accept() { - controller().ignoreAll(); + ignoreAll(); } -void GuiSpellcheckerDialog::add() +void GuiSpellchecker::add() { - controller().insert(); + insert(); } -void GuiSpellcheckerDialog::ignore() +void GuiSpellchecker::ignore() { - controller().check(); + check(); } -void GuiSpellcheckerDialog::replace() +void GuiSpellchecker::replace() { - controller().replace(qstring_to_ucs4(replaceCO->currentText())); + replace(qstring_to_ucs4(replaceCO->currentText())); } -void GuiSpellcheckerDialog::partialUpdate(int state) +void GuiSpellchecker::partialUpdate(int state) { switch (state) { - case ControlSpellchecker::SPELL_PROGRESSED: - spellcheckPR->setValue(controller().getProgress()); + case SPELL_PROGRESSED: + spellcheckPR->setValue(getProgress()); break; - case ControlSpellchecker::SPELL_FOUND_WORD: { - wordED->setText(toqstr(controller().getWord())); + case SPELL_FOUND_WORD: { + wordED->setText(toqstr(getWord())); suggestionsLW->clear(); docstring w; - while (!(w = controller().getSuggestion()).empty()) + while (!(w = getSuggestion()).empty()) suggestionsLW->addItem(toqstr(w)); if (suggestionsLW->count() == 0) @@ -188,6 +218,281 @@ void GuiSpellcheckerDialog::partialUpdate(int state) } } + +static SpellBase * getSpeller(BufferParams const & bp) +{ + string lang = (lyxrc.isp_use_alt_lang) + ? lyxrc.isp_alt_lang + : bp.language->code(); + +#if defined(USE_ASPELL) + if (lyxrc.use_spell_lib) + return new ASpell(bp, lang); +#elif defined(USE_PSPELL) + if (lyxrc.use_spell_lib) + return new PSpell(bp, lang); +#endif + +#if defined(USE_ISPELL) + lang = lyxrc.isp_use_alt_lang ? + lyxrc.isp_alt_lang : bp.language->lang(); + + return new ISpell(bp, lang); +#else + return new SpellBase; +#endif +} + + +bool GuiSpellchecker::initialiseParams(std::string const &) +{ + LYXERR(Debug::GUI, "Spellchecker::initialiseParams"); + + speller_ = getSpeller(buffer().params()); + if (!speller_) + return false; + + // reset values to initial + oldval_ = 0; + newvalue_ = 0; + count_ = 0; + + bool const success = speller_->error().empty(); + + if (!success) { + Alert::error(_("Spellchecker error"), + _("The spellchecker could not be started\n") + + speller_->error()); + delete speller_; + speller_ = 0; + } + + return success; +} + + +void GuiSpellchecker::clearParams() +{ + LYXERR(Debug::GUI, "Spellchecker::clearParams"); + delete speller_; + speller_ = 0; +} + + +static bool isLetter(DocIterator const & dit) +{ + return dit.inTexted() + && dit.inset().allowSpellCheck() + && dit.pos() != dit.lastpos() + && (dit.paragraph().isLetter(dit.pos()) + // We want to pass the ' and escape chars to ispell + || contains(from_utf8(lyxrc.isp_esc_chars + '\''), + dit.paragraph().getChar(dit.pos()))) + && !dit.paragraph().isDeleted(dit.pos()); +} + + +static WordLangTuple nextWord(Cursor & cur, ptrdiff_t & progress) +{ + BufferParams const & bp = cur.bv().buffer().params(); + bool inword = false; + bool ignoreword = false; + cur.resetAnchor(); + docstring word; + string lang_code; + + while (cur.depth()) { + if (isLetter(cur)) { + if (!inword) { + inword = true; + ignoreword = false; + cur.resetAnchor(); + word.clear(); + lang_code = cur.paragraph().getFontSettings(bp, cur.pos()).language()->code(); + } + // Insets like optional hyphens and ligature + // break are part of a word. + if (!cur.paragraph().isInset(cur.pos())) { + char_type const c = cur.paragraph().getChar(cur.pos()); + word += c; + if (isDigit(c)) + ignoreword = true; + } + } else { // !isLetter(cur) + if (inword) + if (!word.empty() && !ignoreword) { + cur.setSelection(); + return WordLangTuple(word, lang_code); + } + inword = false; + } + + cur.forwardPos(); + ++progress; + } + + return WordLangTuple(docstring(), string()); +} + + +void GuiSpellchecker::check() +{ + LYXERR(Debug::GUI, "Check the spelling of a word"); + + SpellBase::Result res = SpellBase::OK; + + Cursor cur = bufferview()->cursor(); + while (cur && cur.pos() && isLetter(cur)) + cur.backwardPos(); + + ptrdiff_t start = 0; + ptrdiff_t total = 0; + DocIterator it = DocIterator(buffer().inset()); + for (start = 0; it != cur; it.forwardPos()) + ++start; + + for (total = start; it; it.forwardPos()) + ++total; + + exitEarly_ = false; + + while (res == SpellBase::OK || res == SpellBase::IGNORED_WORD) { + word_ = nextWord(cur, start); + + // end of document + if (getWord().empty()) { + showSummary(); + exitEarly_ = true; + return; + } + + ++count_; + + // Update slider if and only if value has changed + float progress = total ? float(start)/total : 1; + newvalue_ = int(100.0 * progress); + if (newvalue_!= oldval_) { + LYXERR(Debug::GUI, "Updating spell progress."); + oldval_ = newvalue_; + // set progress bar + partialUpdate(SPELL_PROGRESSED); + } + + // speller might be dead ... + if (!checkAlive()) + return; + + res = speller_->check(word_); + + // ... or it might just be reporting an error + if (!checkAlive()) + return; + } + + LYXERR(Debug::GUI, "Found word \"" << to_utf8(getWord()) << "\""); + + int const size = cur.selEnd().pos() - cur.selBegin().pos(); + cur.pos() -= size; + bufferview()->putSelectionAt(cur, size, false); + // FIXME: if we used a lfun like in find/replace, dispatch would do + // that for us + // FIXME: this Controller is very badly designed... + bufferview()->processUpdateFlags(Update::Force | Update::FitCursor); + + // set suggestions + if (res != SpellBase::OK && res != SpellBase::IGNORED_WORD) { + LYXERR(Debug::GUI, "Found a word needing checking."); + partialUpdate(SPELL_FOUND_WORD); + } +} + + +bool GuiSpellchecker::checkAlive() +{ + if (speller_->alive() && speller_->error().empty()) + return true; + + docstring message; + if (speller_->error().empty()) + message = _("The spellchecker has died for some reason.\n" + "Maybe it has been killed."); + else + message = _("The spellchecker has failed.\n") + speller_->error(); + + slotClose(); + + Alert::error(_("The spellchecker has failed"), message); + return false; +} + + +void GuiSpellchecker::showSummary() +{ + if (!checkAlive() || count_ == 0) { + slotClose(); + return; + } + + docstring message; + if (count_ != 1) + message = bformat(_("%1$d words checked."), count_); + else + message = _("One word checked."); + + slotClose(); + Alert::information(_("Spelling check completed"), message); +} + + +void GuiSpellchecker::replace(docstring const & replacement) +{ + LYXERR(Debug::GUI, "GuiSpellchecker::replace(" + << to_utf8(replacement) << ")"); + cap::replaceSelectionWithString(bufferview()->cursor(), replacement, true); + buffer().markDirty(); + // If we used an LFUN, we would not need that + bufferview()->processUpdateFlags(Update::Force | Update::FitCursor); + // fix up the count + --count_; + check(); +} + + +void GuiSpellchecker::replaceAll(docstring const & replacement) +{ + // TODO: add to list + replace(replacement); +} + + +void GuiSpellchecker::insert() +{ + speller_->insert(word_); + check(); +} + + +docstring GuiSpellchecker::getSuggestion() const +{ + return speller_->nextMiss(); +} + + +docstring GuiSpellchecker::getWord() const +{ + return word_.word(); +} + + +void GuiSpellchecker::ignoreAll() +{ + speller_->accept(word_); + check(); +} + + +Dialog * createGuiSpellchecker(GuiView & lv) { return new GuiSpellchecker(lv); } + } // namespace frontend } // namespace lyx