]> git.lyx.org Git - lyx.git/blobdiff - src/frontends/qt4/GuiSpellchecker.cpp
Transfer some more dialog related code from core to frontend:
[lyx.git] / src / frontends / qt4 / GuiSpellchecker.cpp
index 52302e418b5df6f6ea3fbc8018a52eca30124baf..daff427e85e51096ba85cf9daba6416dbf7f40e0 100644 (file)
@@ -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.
  */
 
 #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 <QProgressBar>
 #include <QLineEdit>
 #include <QPushButton>
 #include <QTextCharFormat>
 #include <QTextDocument>
 
+#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<ControlSpellchecker &>(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