X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2Ffrontends%2Fqt4%2FGuiSpellchecker.cpp;h=537d9d79b50184b8e652d75aac0ab3369db54e2e;hb=b7f6b979d0f889f08e735f35378bb20ba3788b4b;hp=356b0becb4e624e28dd787b2471c96981fc230b9;hpb=a1cec91afaca91968b46e695533c10ad2a3f73d3;p=lyx.git diff --git a/src/frontends/qt4/GuiSpellchecker.cpp b/src/frontends/qt4/GuiSpellchecker.cpp index 356b0becb4..537d9d79b5 100644 --- a/src/frontends/qt4/GuiSpellchecker.cpp +++ b/src/frontends/qt4/GuiSpellchecker.cpp @@ -4,6 +4,8 @@ * Licence details can be found in the file COPYING. * * \author John Levon + * \author Edwin Leuven + * \author Abdelrazak Younes * * Full author contact details are available in file CREDITS. */ @@ -11,205 +13,617 @@ #include #include "GuiSpellchecker.h" -#include "Qt2BC.h" -#include "qt_helpers.h" +#include "GuiApplication.h" -#include "controllers/ControlSpellchecker.h" +#include "qt_helpers.h" -#include -#include -#include -#include +#include "ui_SpellcheckerUi.h" + +#include "Buffer.h" +#include "BufferParams.h" +#include "BufferView.h" +#include "buffer_funcs.h" +#include "Cursor.h" +#include "Text.h" +#include "CutAndPaste.h" +#include "FuncRequest.h" +#include "Language.h" +#include "LyX.h" +#include "LyXRC.h" +#include "lyxfind.h" +#include "Paragraph.h" +#include "WordLangTuple.h" + +#include "support/debug.h" +#include "support/docstring.h" +#include "support/docstring_list.h" +#include "support/ExceptionMessage.h" +#include "support/gettext.h" +#include "support/lstrings.h" +#include "support/textutils.h" + +#include #include -#include -#include -#include -#include +#include +#include "SpellChecker.h" -using std::string; +#include "frontends/alert.h" + +using namespace std; +using namespace lyx::support; namespace lyx { namespace frontend { -///////////////////////////////////////////////////////////////////// -// -// GuiSpellCheckerDialog -// -///////////////////////////////////////////////////////////////////// - -GuiSpellcheckerDialog::GuiSpellcheckerDialog(GuiSpellchecker * form) - : form_(form) +struct SpellcheckerWidget::Private { - setupUi(this); + Private(SpellcheckerWidget * parent, DockView * dv, GuiView * gv) + : p(parent), dv_(dv), gv_(gv), incheck_(false), wrap_around_(false) {} + /// update from controller + void updateSuggestions(docstring_list & words); + /// move to next position after current word + void forward(); + /// check text until next misspelled/unknown word + void check(); + /// close the spell checker dialog + void hide() const; + /// make/restore a selection between from and to + void setSelection(DocIterator const & from, DocIterator const & to) const; + /// if no selection was checked: + /// ask the user if the check should start over + bool continueFromBeginning(); + /// set the given language in language chooser + void setLanguage(Language const * lang); + /// test and set guard flag + bool inCheck() { + if (incheck_) + return true; + incheck_ = true; + return false; + } + void canCheck() { incheck_ = false; } + /// check for wrap around + void wrapAround(bool flag) { + wrap_around_ = flag; + if (flag) { + end_ = start_; + } + } + /// test for existing association with a document buffer + /// and test for already active check + bool disabled() { + return gv_->documentBufferView() == 0 || inCheck(); + } + /// the cursor position of the buffer view + DocIterator const cursor() const; + /// status checks + bool isCurrentBuffer(DocIterator const & cursor) const; + bool isWrapAround(DocIterator const & cursor) const; + bool isWrapAround() const { return wrap_around_; } + bool atLastPos(DocIterator const & cursor) const; + /// validate the cached doc iterators + /// The spell checker dialog is not modal. + /// The user may change the buffer being checked and break the iterators. + void fixPositionsIfBroken(); + /// + Ui::SpellcheckerUi ui; + /// + SpellcheckerWidget * p; + /// + DockView * dv_; + /// + GuiView * gv_; + /// current word being checked and lang code + WordLangTuple word_; + /// cursor position where spell checking starts + DocIterator start_; + /// range to spell check + /// for selection both are non-empty + /// after wrap around the start position becomes the end + DocIterator begin_; + DocIterator end_; + /// + bool incheck_; + /// + bool wrap_around_; +}; + + +SpellcheckerWidget::SpellcheckerWidget(GuiView * gv, DockView * dv, QWidget * parent) + : QTabWidget(parent), d(new Private(this, dv, gv)) +{ + d->ui.setupUi(this); - connect(closePB, SIGNAL(clicked()), form, SLOT(slotClose())); + connect(d->ui.suggestionsLW, SIGNAL(itemDoubleClicked(QListWidgetItem*)), + this, SLOT(on_replacePB_clicked())); - 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(suggestionsLW, SIGNAL(itemDoubleClicked(QListWidgetItem*)), - this, SLOT(replaceClicked() ) ); - connect(suggestionsLW, SIGNAL(itemClicked(QListWidgetItem*)), - this, SLOT(suggestionChanged(QListWidgetItem*))); -} + // language + QAbstractItemModel * language_model = guiApp->languageModel(); + // FIXME: it would be nice if sorting was enabled/disabled via a checkbox. + language_model->sort(0); + d->ui.languageCO->setModel(language_model); + d->ui.languageCO->setModelColumn(1); + d->ui.wordED->setReadOnly(true); -void GuiSpellcheckerDialog::acceptClicked() -{ - form_->accept(); + d->ui.suggestionsLW->installEventFilter(this); } -void GuiSpellcheckerDialog::addClicked() -{ - form_->add(); -} -void GuiSpellcheckerDialog::replaceClicked() +SpellcheckerWidget::~SpellcheckerWidget() { - form_->replace(); + delete d; } -void GuiSpellcheckerDialog::ignoreClicked() + +bool SpellcheckerWidget::eventFilter(QObject *obj, QEvent *event) { - form_->ignore(); + if (obj == d->ui.suggestionsLW && event->type() == QEvent::KeyPress) { + QKeyEvent *e = static_cast (event); + if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) { + if (d->ui.suggestionsLW->currentItem()) { + on_suggestionsLW_itemClicked(d->ui.suggestionsLW->currentItem()); + on_replacePB_clicked(); + } + return true; + } else if (e->key() == Qt::Key_Right) { + if (d->ui.suggestionsLW->currentItem()) + on_suggestionsLW_itemClicked(d->ui.suggestionsLW->currentItem()); + return true; + } + } + // standard event processing + return QWidget::eventFilter(obj, event); } -void GuiSpellcheckerDialog::suggestionChanged(QListWidgetItem * item) + +void SpellcheckerWidget::on_suggestionsLW_itemClicked(QListWidgetItem * item) { - if (replaceCO->count() != 0) - replaceCO->setItemText(0, item->text()); + if (d->ui.replaceCO->count() != 0) + d->ui.replaceCO->setItemText(0, item->text()); else - replaceCO->addItem(item->text()); + d->ui.replaceCO->addItem(item->text()); - replaceCO->setCurrentIndex(0); + d->ui.replaceCO->setCurrentIndex(0); } -void GuiSpellcheckerDialog::replaceChanged(const QString & str) + +void SpellcheckerWidget::on_replaceCO_highlighted(const QString & str) { - if (suggestionsLW->currentItem()->text() == str) + QListWidget * lw = d->ui.suggestionsLW; + if (lw->currentItem() && lw->currentItem()->text() == str) return; - for (int i = 0; i < suggestionsLW->count(); ++i) { - if (suggestionsLW->item(i)->text() == str) { - suggestionsLW->setCurrentRow(i); + for (int i = 0; i != lw->count(); ++i) { + if (lw->item(i)->text() == str) { + lw->setCurrentRow(i); break; } } } -void GuiSpellcheckerDialog::closeEvent(QCloseEvent * e) +void SpellcheckerWidget::updateView() { - form_->slotWMHide(); - e->accept(); + BufferView * bv = d->gv_->documentBufferView(); + // we need a buffer view and the buffer has to be writable + bool const enabled = bv != 0 && !bv->buffer().isReadonly(); + setEnabled(enabled); + if (enabled && hasFocus()) { + Cursor const & cursor = bv->cursor(); + if (d->start_.empty() || !d->isCurrentBuffer(cursor)) { + if (cursor.selection()) { + d->begin_ = cursor.selectionBegin(); + d->end_ = cursor.selectionEnd(); + d->start_ = d->begin_; + bv->cursor().setCursor(d->start_); + } else { + d->begin_ = DocIterator(); + d->end_ = DocIterator(); + d->start_ = cursor; + } + d->wrapAround(false); + d->check(); + } + } } +DocIterator const SpellcheckerWidget::Private::cursor() const +{ + BufferView * bv = gv_->documentBufferView(); + return bv ? bv->cursor() : DocIterator(); +} -void GuiSpellcheckerDialog::reject() +bool SpellcheckerWidget::Private::continueFromBeginning() { - form_->slotWMHide(); - QDialog::reject(); + DocIterator const current_ = cursor(); + if (isCurrentBuffer(current_) && !begin_.empty()) { + // selection was checked + // start over from beginning makes no sense + fixPositionsIfBroken(); + hide(); + if (current_ == start_) { + // no errors found... tell the user the good news + // so there is some feedback + QMessageBox::information(p, + qt_("Spell Checker"), + qt_("Spell check of the selection done, " + "did not find any errors.")); + } + return false; + } + QMessageBox::StandardButton const answer = QMessageBox::question(p, + qt_("Spell Checker"), + qt_("We reached the end of the document, would you like to " + "continue from the beginning?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer == QMessageBox::No) { + fixPositionsIfBroken(); + hide(); + return false; + } + // there is no selection, start over from the beginning now + wrapAround(true); + dispatch(FuncRequest(LFUN_BUFFER_BEGIN)); + return true; } +bool SpellcheckerWidget::Private::isCurrentBuffer(DocIterator const & cursor) const +{ + return start_.buffer() == cursor.buffer(); +} +bool SpellcheckerWidget::Private::atLastPos(DocIterator const & cursor) const +{ + bool const valid_end = !end_.empty(); + return cursor.depth() <= 1 && ( + cursor.atEnd() || + (valid_end && isCurrentBuffer(cursor) && cursor >= end_)); +} -///////////////////////////////////////////////////////////////////// -// -// GuiSpellChecker -// -///////////////////////////////////////////////////////////////////// +bool SpellcheckerWidget::Private::isWrapAround(DocIterator const & cursor) const +{ + return wrap_around_ && isCurrentBuffer(cursor) && start_ < cursor; +} + +void SpellcheckerWidget::Private::fixPositionsIfBroken() +{ + DocIterator const current_ = cursor(); + if (!isCurrentBuffer(current_)) { + LYXERR(Debug::GUI, "wrong document of current cursor position " << start_); + start_ = current_; + begin_ = DocIterator(); + end_ = DocIterator(); + } + if (start_.fixIfBroken()) + LYXERR(Debug::GUI, "broken start position fixed " << start_); + if (begin_.fixIfBroken()) { + LYXERR(Debug::GUI, "broken selection begin position fixed " << begin_); + begin_ = DocIterator(); + end_ = DocIterator(); + } + if (end_.fixIfBroken()) + LYXERR(Debug::GUI, "broken selection end position fixed " << end_); +} + +void SpellcheckerWidget::Private::hide() const +{ + BufferView * bv = gv_->documentBufferView(); + Cursor & bvcur = bv->cursor(); + dv_->hide(); + if (isCurrentBuffer(bvcur)) { + if (!begin_.empty() && !end_.empty()) { + // restore previous selection + setSelection(begin_, end_); + } else { + // restore cursor position + bvcur.setCursor(start_); + bvcur.clearSelection(); + bv->processUpdateFlags(Update::Force | Update::FitCursor); + } + } +} + +void SpellcheckerWidget::Private::setSelection( + DocIterator const & from, DocIterator const & to) const +{ + BufferView * bv = gv_->documentBufferView(); + DocIterator end = to; + + if (from.pit() != end.pit()) { + // there are multiple paragraphs in selection + Cursor & bvcur = bv->cursor(); + bvcur.setCursor(from); + bvcur.clearSelection(); + bvcur.selection(true); + bvcur.setCursor(end); + bvcur.selection(true); + } else { + // FIXME LFUN + // If we used a LFUN, dispatch would do all of this for us + int const size = end.pos() - from.pos(); + bv->putSelectionAt(from, size, false); + } + bv->processUpdateFlags(Update::Force | Update::FitCursor); +} + +void SpellcheckerWidget::Private::forward() +{ + DocIterator const from = cursor(); + + dispatch(FuncRequest(LFUN_ESCAPE)); + fixPositionsIfBroken(); + if (!atLastPos(cursor())) { + dispatch(FuncRequest(LFUN_CHAR_FORWARD)); + } + if (atLastPos(cursor())) { + return; + } + if (from == cursor()) { + //FIXME we must be at the end of a cell + dispatch(FuncRequest(LFUN_CHAR_FORWARD)); + } + if (isWrapAround(cursor())) { + hide(); + } +} -typedef QController > - SpellcheckerBase; -GuiSpellchecker::GuiSpellchecker(Dialog & parent) - : SpellcheckerBase(parent, _("Spellchecker")) -{} +void SpellcheckerWidget::on_languageCO_activated(int index) +{ + string const lang = + fromqstr(d->ui.languageCO->itemData(index).toString()); + if (!d->word_.lang() || d->word_.lang()->lang() == lang) + // nothing changed + return; + dispatch(FuncRequest(LFUN_LANGUAGE, lang)); + d->check(); +} -void GuiSpellchecker::build_dialog() +bool SpellcheckerWidget::initialiseParams(std::string const &) { - dialog_.reset(new GuiSpellcheckerDialog(this)); + BufferView * bv = d->gv_->documentBufferView(); + if (bv == 0) + return false; + std::set languages = + bv->buffer().masterBuffer()->getLanguages(); + if (!languages.empty()) + d->setLanguage(*languages.begin()); + d->start_ = DocIterator(); + d->wrapAround(false); + d->canCheck(); + return true; +} - bcview().setCancel(dialog_->closePB); - dialog_->wordED->setReadOnly(true); + +void SpellcheckerWidget::on_ignoreAllPB_clicked() +{ + /// ignore all occurrences of word + if (d->disabled()) + return; + LYXERR(Debug::GUI, "Spellchecker: ignore all button"); + if (d->word_.lang() && !d->word_.word().empty()) + theSpellChecker()->accept(d->word_); + d->forward(); + d->check(); + d->canCheck(); } -void GuiSpellchecker::update_contents() +void SpellcheckerWidget::on_addPB_clicked() { - if (isVisible() || controller().exitEarly()) - controller().check(); + /// insert word in personal dictionary + if (d->disabled()) + return; + LYXERR(Debug::GUI, "Spellchecker: add word button"); + theSpellChecker()->insert(d->word_); + d->forward(); + d->check(); + d->canCheck(); } -void GuiSpellchecker::accept() +void SpellcheckerWidget::on_ignorePB_clicked() { - controller().ignoreAll(); + /// ignore this occurrence of word + if (d->disabled()) + return; + LYXERR(Debug::GUI, "Spellchecker: ignore button"); + d->forward(); + d->check(); + d->canCheck(); } -void GuiSpellchecker::add() +void SpellcheckerWidget::on_findNextPB_clicked() { - controller().insert(); + if (d->disabled()) + return; + docstring const textfield = qstring_to_ucs4(d->ui.wordED->text()); + docstring const datastring = find2string(textfield, + true, true, true); + LYXERR(Debug::GUI, "Spellchecker: find next (" << textfield << ")"); + dispatch(FuncRequest(LFUN_WORD_FIND, datastring)); + d->canCheck(); } -void GuiSpellchecker::ignore() +void SpellcheckerWidget::on_replacePB_clicked() { - controller().check(); + if (d->disabled()) + return; + docstring const textfield = qstring_to_ucs4(d->ui.wordED->text()); + docstring const replacement = qstring_to_ucs4(d->ui.replaceCO->currentText()); + docstring const datastring = + replace2string(replacement, textfield, + true, // case sensitive + true, // match word + false, // all words + true, // forward + false); // find next + + LYXERR(Debug::GUI, "Replace (" << replacement << ")"); + dispatch(FuncRequest(LFUN_WORD_REPLACE, datastring)); + d->forward(); + d->check(); + d->canCheck(); } -void GuiSpellchecker::replace() +void SpellcheckerWidget::on_replaceAllPB_clicked() { - controller().replace(qstring_to_ucs4(dialog_->replaceCO->currentText())); + if (d->disabled()) + return; + docstring const textfield = qstring_to_ucs4(d->ui.wordED->text()); + docstring const replacement = qstring_to_ucs4(d->ui.replaceCO->currentText()); + docstring const datastring = + replace2string(replacement, textfield, + true, // case sensitive + true, // match word + true, // all words + true, // forward + false); // find next + + LYXERR(Debug::GUI, "Replace all (" << replacement << ")"); + dispatch(FuncRequest(LFUN_WORD_REPLACE, datastring)); + d->forward(); + // replace all wraps around + d->wrapAround(true); + d->check(); // continue spellchecking + d->canCheck(); } -void GuiSpellchecker::partialUpdate(int s) +void SpellcheckerWidget::Private::updateSuggestions(docstring_list & words) { - ControlSpellchecker::State const state = - static_cast(s); + QString const suggestion = toqstr(word_.word()); + ui.wordED->setText(suggestion); + QListWidget * lw = ui.suggestionsLW; + lw->clear(); - switch (state) { + if (words.empty()) { + p->on_suggestionsLW_itemClicked(new QListWidgetItem(suggestion)); + return; + } + for (size_t i = 0; i != words.size(); ++i) + lw->addItem(toqstr(words[i])); - case ControlSpellchecker::SPELL_PROGRESSED: - dialog_->spellcheckPR->setValue(controller().getProgress()); - break; + p->on_suggestionsLW_itemClicked(lw->item(0)); + lw->setCurrentRow(0); +} - case ControlSpellchecker::SPELL_FOUND_WORD: { - dialog_->wordED->setText(toqstr(controller().getWord())); - dialog_->suggestionsLW->clear(); - docstring w; - while (!(w = controller().getSuggestion()).empty()) { - dialog_->suggestionsLW->addItem(toqstr(w)); +void SpellcheckerWidget::Private::setLanguage(Language const * lang) +{ + int const pos = ui.languageCO->findData(toqstr(lang->lang())); + if (pos != -1) + ui.languageCO->setCurrentIndex(pos); +} + + +void SpellcheckerWidget::Private::check() +{ + BufferView * bv = gv_->documentBufferView(); + if (!bv || bv->buffer().text().empty()) + return; + + fixPositionsIfBroken(); + + SpellChecker * speller = theSpellChecker(); + if (speller && !speller->hasDictionary(bv->buffer().language())) { + int dsize = speller->numDictionaries(); + if (0 == dsize) { + hide(); + QMessageBox::information(p, + qt_("Spell Checker"), + qt_("Spell checker has no dictionaries.")); + return; } + } - if (dialog_->suggestionsLW->count() == 0) { - dialog_->suggestionChanged(new QListWidgetItem(dialog_->wordED->text())); - } else { - dialog_->suggestionChanged(dialog_->suggestionsLW->item(0)); + DocIterator from = bv->cursor(); + DocIterator to = isCurrentBuffer(from) ? end_ : doc_iterator_end(&bv->buffer()); + WordLangTuple word_lang; + docstring_list suggestions; + + LYXERR(Debug::GUI, "Spellchecker: start check at " << from); + try { + bv->buffer().spellCheck(from, to, word_lang, suggestions); + } catch (ExceptionMessage const & message) { + if (message.type_ == WarningException) { + Alert::warning(message.title_, message.details_); + return; } + throw message; + } - dialog_->suggestionsLW->setCurrentRow(0); + // end of document or selection? + if (atLastPos(from)) { + if (isWrapAround()) { + hide(); + return; + } + if (continueFromBeginning()) + check(); + return; } - break; + if (isWrapAround(from)) { + hide(); + return; } + + word_ = word_lang; + + // set suggestions + updateSuggestions(suggestions); + // set language + if (!word_lang.lang()) + return; + setLanguage(word_lang.lang()); + // mark misspelled word + setSelection(from, to); +} + + +GuiSpellchecker::GuiSpellchecker(GuiView & parent, + Qt::DockWidgetArea area, Qt::WindowFlags flags) + : DockView(parent, "spellchecker", qt_("Spellchecker"), + area, flags) +{ + widget_ = new SpellcheckerWidget(&parent, this); + setWidget(widget_); + setFocusProxy(widget_); } + +GuiSpellchecker::~GuiSpellchecker() +{ + setFocusProxy(0); + delete widget_; +} + + +void GuiSpellchecker::updateView() +{ + widget_->updateView(); +} + + +Dialog * createGuiSpellchecker(GuiView & lv) +{ + GuiSpellchecker * gui = new GuiSpellchecker(lv, Qt::RightDockWidgetArea); +#ifdef Q_OS_MAC + gui->setFloating(true); +#endif + return gui; +} + + } // namespace frontend } // namespace lyx -#include "GuiSpellchecker_moc.cpp" +#include "moc_GuiSpellchecker.cpp"