]> 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 356b0becb4e624e28dd787b2471c96981fc230b9..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 <config.h>
 
 #include "GuiSpellchecker.h"
-#include "Qt2BC.h"
+
 #include "qt_helpers.h"
 
-#include "controllers/ControlSpellchecker.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 <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
-//
-/////////////////////////////////////////////////////////////////////
+using support::bformat;
+using support::contains;
 
-
-GuiSpellcheckerDialog::GuiSpellcheckerDialog(GuiSpellchecker * form)
-       : form_(form)
+GuiSpellchecker::GuiSpellchecker(GuiView & lv)
+       : GuiDialog(lv, "spellchecker"), exitEarly_(false),
+         oldval_(0), newvalue_(0), count_(0), speller_(0)
 {
        setupUi(this);
+       setViewTitle(_("Spellchecker"));
+
+       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(closePB, SIGNAL(clicked()), form, SLOT(slotClose()));
-
-       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*)));
-}
 
+       wordED->setReadOnly(true);
 
-void GuiSpellcheckerDialog::acceptClicked()
-{
-       form_->accept();
+       bc().setPolicy(ButtonPolicy::NoRepeatedApplyReadOnlyPolicy);
+       bc().setCancel(closePB);
 }
 
-void GuiSpellcheckerDialog::addClicked()
-{
-       form_->add();
-}
 
-void GuiSpellcheckerDialog::replaceClicked()
+GuiSpellchecker::~GuiSpellchecker()
 {
-       form_->replace();
+       delete speller_;
 }
 
-void GuiSpellcheckerDialog::ignoreClicked()
-{
-       form_->ignore();
-}
 
-void GuiSpellcheckerDialog::suggestionChanged(QListWidgetItem * item)
+void GuiSpellchecker::suggestionChanged(QListWidgetItem * item)
 {
        if (replaceCO->count() != 0)
                replaceCO->setItemText(0, item->text());
@@ -93,12 +109,13 @@ void GuiSpellcheckerDialog::suggestionChanged(QListWidgetItem * item)
        replaceCO->setCurrentIndex(0);
 }
 
-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;
@@ -107,108 +124,375 @@ void GuiSpellcheckerDialog::replaceChanged(const QString & str)
 }
 
 
-void GuiSpellcheckerDialog::closeEvent(QCloseEvent * e)
+void GuiSpellchecker::closeEvent(QCloseEvent * e)
 {
-       form_->slotWMHide();
-       e->accept();
+       slotClose();
+       GuiDialog::closeEvent(e);
 }
 
 
-void GuiSpellcheckerDialog::reject()
+void GuiSpellchecker::reject()
 {
-       form_->slotWMHide();
+       slotClose();
        QDialog::reject();
 }
 
 
+void GuiSpellchecker::updateContents()
+{
+       // 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();
+       }
+}
 
-/////////////////////////////////////////////////////////////////////
-//
-// GuiSpellChecker
-//
-/////////////////////////////////////////////////////////////////////
 
-typedef QController<ControlSpellchecker, GuiView<GuiSpellcheckerDialog> >
-       SpellcheckerBase;
+void GuiSpellchecker::accept()
+{
+       ignoreAll();
+}
 
-GuiSpellchecker::GuiSpellchecker(Dialog & parent)
-       : SpellcheckerBase(parent, _("Spellchecker"))
-{}
+
+void GuiSpellchecker::add()
+{
+       insert();
+}
 
 
-void GuiSpellchecker::build_dialog()
+void GuiSpellchecker::ignore()
 {
-       dialog_.reset(new GuiSpellcheckerDialog(this));
+       check();
+}
+
 
-       bcview().setCancel(dialog_->closePB);
-       dialog_->wordED->setReadOnly(true);
+void GuiSpellchecker::replace()
+{
+       replace(qstring_to_ucs4(replaceCO->currentText()));
 }
 
 
-void GuiSpellchecker::update_contents()
+void GuiSpellchecker::partialUpdate(int state)
 {
-       if (isVisible() || controller().exitEarly())
-               controller().check();
+       switch (state) {
+               case SPELL_PROGRESSED:
+                       spellcheckPR->setValue(getProgress());
+                       break;
+
+               case SPELL_FOUND_WORD: {
+                       wordED->setText(toqstr(getWord()));
+                       suggestionsLW->clear();
+
+                       docstring w;
+                       while (!(w = getSuggestion()).empty())
+                               suggestionsLW->addItem(toqstr(w));
+
+                       if (suggestionsLW->count() == 0)
+                               suggestionChanged(new QListWidgetItem(wordED->text()));
+                       else
+                               suggestionChanged(suggestionsLW->item(0));
+
+                       suggestionsLW->setCurrentRow(0);
+                       break;
+               }
+       }
 }
 
 
-void GuiSpellchecker::accept()
+static SpellBase * getSpeller(BufferParams const & bp)
 {
-       controller().ignoreAll();
+       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
 }
 
 
-void GuiSpellchecker::add()
+bool GuiSpellchecker::initialiseParams(std::string const &)
 {
-       controller().insert();
+       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::ignore()
+void GuiSpellchecker::clearParams()
 {
-       controller().check();
+       LYXERR(Debug::GUI, "Spellchecker::clearParams");
+       delete speller_;
+       speller_ = 0;
 }
 
 
-void GuiSpellchecker::replace()
+static bool isLetter(DocIterator const & dit)
 {
-       controller().replace(qstring_to_ucs4(dialog_->replaceCO->currentText()));
+       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());
 }
 
 
-void GuiSpellchecker::partialUpdate(int s)
+static WordLangTuple nextWord(Cursor & cur, ptrdiff_t & progress)
 {
-       ControlSpellchecker::State const state =
-               static_cast<ControlSpellchecker::State>(s);
+       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());
+}
 
-       switch (state) {
 
-       case ControlSpellchecker::SPELL_PROGRESSED:
-               dialog_->spellcheckPR->setValue(controller().getProgress());
-               break;
+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;
 
-       case ControlSpellchecker::SPELL_FOUND_WORD: {
-               dialog_->wordED->setText(toqstr(controller().getWord()));
-               dialog_->suggestionsLW->clear();
+       for (total = start; it; it.forwardPos())
+               ++total;
 
-               docstring w;
-               while (!(w = controller().getSuggestion()).empty()) {
-                       dialog_->suggestionsLW->addItem(toqstr(w));
+       exitEarly_ = false;
+
+       while (res == SpellBase::OK || res == SpellBase::IGNORED_WORD) {
+               word_ = nextWord(cur, start);
+
+               // end of document
+               if (getWord().empty()) {
+                       showSummary();
+                       exitEarly_ = true;
+                       return;
                }
 
-               if (dialog_->suggestionsLW->count() == 0) {
-                       dialog_->suggestionChanged(new QListWidgetItem(dialog_->wordED->text()));
-               } else {
-                       dialog_->suggestionChanged(dialog_->suggestionsLW->item(0));
+               ++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);
                }
 
-               dialog_->suggestionsLW->setCurrentRow(0);
+               // speller might be dead ...
+               if (!checkAlive())
+                       return;
+
+               res = speller_->check(word_);
+
+               // ... or it might just be reporting an error
+               if (!checkAlive())
+                       return;
        }
-               break;
 
+       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