X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2Ffrontends%2Fqt4%2FGuiSpellchecker.cpp;h=537d9d79b50184b8e652d75aac0ab3369db54e2e;hb=b7f6b979d0f889f08e735f35378bb20ba3788b4b;hp=b5dadeb5f6000d9857d87625e0dd70062ed6fc11;hpb=193074b3847b0364c8270c96614993e4500c938e;p=lyx.git diff --git a/src/frontends/qt4/GuiSpellchecker.cpp b/src/frontends/qt4/GuiSpellchecker.cpp index b5dadeb5f6..537d9d79b5 100644 --- a/src/frontends/qt4/GuiSpellchecker.cpp +++ b/src/frontends/qt4/GuiSpellchecker.cpp @@ -5,6 +5,7 @@ * * \author John Levon * \author Edwin Leuven + * \author Abdelrazak Younes * * Full author contact details are available in file CREDITS. */ @@ -12,19 +13,26 @@ #include #include "GuiSpellchecker.h" +#include "GuiApplication.h" #include "qt_helpers.h" +#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" @@ -34,7 +42,9 @@ #include "support/lstrings.h" #include "support/textutils.h" +#include #include +#include #include "SpellChecker.h" @@ -47,223 +57,572 @@ namespace lyx { namespace frontend { -GuiSpellchecker::GuiSpellchecker(GuiView & lv) - : GuiDialog(lv, "spellchecker", qt_("Spellchecker")), - progress_(0), count_(0) +struct SpellcheckerWidget::Private +{ + 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)) { - setupUi(this); + d->ui.setupUi(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(d->ui.suggestionsLW, SIGNAL(itemDoubleClicked(QListWidgetItem*)), + this, SLOT(on_replacePB_clicked())); - connect(replaceCO, SIGNAL(highlighted(QString)), - this, SLOT(replaceChanged(QString))); - connect(suggestionsLW, SIGNAL(itemDoubleClicked(QListWidgetItem*)), - this, SLOT(replace())); - 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); - wordED->setReadOnly(true); + d->ui.wordED->setReadOnly(true); - bc().setPolicy(ButtonPolicy::NoRepeatedApplyReadOnlyPolicy); - bc().setCancel(closePB); + d->ui.suggestionsLW->installEventFilter(this); } -void GuiSpellchecker::suggestionChanged(QListWidgetItem * item) +SpellcheckerWidget::~SpellcheckerWidget() { - if (replaceCO->count() != 0) - replaceCO->setItemText(0, item->text()); + delete d; +} + + +bool SpellcheckerWidget::eventFilter(QObject *obj, QEvent *event) +{ + 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 SpellcheckerWidget::on_suggestionsLW_itemClicked(QListWidgetItem * item) +{ + 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 GuiSpellchecker::replaceChanged(const QString & str) +void SpellcheckerWidget::on_replaceCO_highlighted(const QString & str) { - if (suggestionsLW->currentItem() - && 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 GuiSpellchecker::reject() +void SpellcheckerWidget::updateView() +{ + 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(); +} + +bool SpellcheckerWidget::Private::continueFromBeginning() +{ + 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_)); +} + +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() { - slotClose(); - QDialog::reject(); + 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(); + } } -void GuiSpellchecker::updateContents() +void SpellcheckerWidget::on_languageCO_activated(int index) { - if (hasFocus()) - check(); + 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::accept() +bool SpellcheckerWidget::initialiseParams(std::string const &) { - theSpellChecker()->accept(word_); - check(); + 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; +} + + +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 SpellcheckerWidget::on_addPB_clicked() +{ + /// 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 SpellcheckerWidget::on_ignorePB_clicked() +{ + /// 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() { - theSpellChecker()->insert(word_); - check(); + 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() { - 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() { - replace(qstring_to_ucs4(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::updateSuggestions(docstring_list & words) +void SpellcheckerWidget::Private::updateSuggestions(docstring_list & words) { - wordED->setText(toqstr(word_.word())); - suggestionsLW->clear(); + QString const suggestion = toqstr(word_.word()); + ui.wordED->setText(suggestion); + QListWidget * lw = ui.suggestionsLW; + lw->clear(); if (words.empty()) { - suggestionChanged(new QListWidgetItem(wordED->text())); + p->on_suggestionsLW_itemClicked(new QListWidgetItem(suggestion)); return; } for (size_t i = 0; i != words.size(); ++i) - suggestionsLW->addItem(toqstr(words[i])); + lw->addItem(toqstr(words[i])); - suggestionChanged(suggestionsLW->item(0)); - suggestionsLW->setCurrentRow(0); + p->on_suggestionsLW_itemClicked(lw->item(0)); + lw->setCurrentRow(0); } -bool GuiSpellchecker::initialiseParams(string const &) +void SpellcheckerWidget::Private::setLanguage(Language const * lang) { - LYXERR(Debug::GUI, "Spellchecker::initialiseParams"); - - if (!theSpellChecker()) - return false; - - DocIterator const begin = doc_iterator_begin(&buffer()); - Cursor const & cur = bufferview()->cursor(); - progress_ = countWords(begin, cur); - total_ = progress_ + countWords(cur, doc_iterator_end(&buffer())); - count_ = 0; - return true; + int const pos = ui.languageCO->findData(toqstr(lang->lang())); + if (pos != -1) + ui.languageCO->setCurrentIndex(pos); } -void GuiSpellchecker::check() +void SpellcheckerWidget::Private::check() { - LYXERR(Debug::GUI, "Check the spelling of a word"); + BufferView * bv = gv_->documentBufferView(); + if (!bv || bv->buffer().text().empty()) + return; + + fixPositionsIfBroken(); - DocIterator from = bufferview()->cursor(); - DocIterator to; + 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; + } + } + + DocIterator from = bv->cursor(); + DocIterator to = isCurrentBuffer(from) ? end_ : doc_iterator_end(&bv->buffer()); WordLangTuple word_lang; docstring_list suggestions; - int progress; + LYXERR(Debug::GUI, "Spellchecker: start check at " << from); try { - progress = buffer().spellCheck(from, to, word_lang, suggestions); + bv->buffer().spellCheck(from, to, word_lang, suggestions); } catch (ExceptionMessage const & message) { if (message.type_ == WarningException) { Alert::warning(message.title_, message.details_); - slotClose(); return; } throw message; } - LYXERR(Debug::GUI, "Found word \"" << word_lang.word() << "\""); - count_ += progress; - progress_ += progress; - // end of document - if (from == to) { - showSummary(); + // end of document or selection? + if (atLastPos(from)) { + if (isWrapAround()) { + hide(); + return; + } + if (continueFromBeginning()) + check(); + return; + } + + if (isWrapAround(from)) { + hide(); return; } - if (!isVisible()) - show(); word_ = word_lang; - int const progress_bar = total_ - ? int(100.0 * float(progress_)/total_) : 100; - LYXERR(Debug::GUI, "Updating spell progress."); - // set progress bar - spellcheckPR->setValue(progress_bar); // set suggestions updateSuggestions(suggestions); - - // FIXME: if we used a lfun like in find/replace, dispatch would do - // that for us - int const size = to.pos() - from.pos(); - BufferView * bv = const_cast(bufferview()); - bv->putSelectionAt(from, size, false); + // set language + if (!word_lang.lang()) + return; + setLanguage(word_lang.lang()); + // mark misspelled word + setSelection(from, to); } -void GuiSpellchecker::showSummary() +GuiSpellchecker::GuiSpellchecker(GuiView & parent, + Qt::DockWidgetArea area, Qt::WindowFlags flags) + : DockView(parent, "spellchecker", qt_("Spellchecker"), + area, flags) { - if (count_ == 0) { - slotClose(); - return; - } + widget_ = new SpellcheckerWidget(&parent, this); + setWidget(widget_); + setFocusProxy(widget_); +} - docstring message; - if (count_ != 1) - message = bformat(_("%1$d words checked."), count_); - else - message = _("One word checked."); - slotClose(); - Alert::information(_("Spelling check completed"), message); +GuiSpellchecker::~GuiSpellchecker() +{ + setFocusProxy(0); + delete widget_; } -void GuiSpellchecker::replace(docstring const & replacement) +void GuiSpellchecker::updateView() { - LYXERR(Debug::GUI, "GuiSpellchecker::replace(" - << to_utf8(replacement) << ")"); - BufferView * bv = const_cast(bufferview()); - cap::replaceSelectionWithString(bv->cursor(), replacement, true); - bv->buffer().markDirty(); - // If we used an LFUN, we would not need that - bv->processUpdateFlags(Update::Force | Update::FitCursor); - // fix up the count - --count_; - check(); + widget_->updateView(); } -void GuiSpellchecker::replaceAll(docstring const & replacement) +Dialog * createGuiSpellchecker(GuiView & lv) { - // TODO: add to list - replace(replacement); + GuiSpellchecker * gui = new GuiSpellchecker(lv, Qt::RightDockWidgetArea); +#ifdef Q_OS_MAC + gui->setFloating(true); +#endif + return gui; } -Dialog * createGuiSpellchecker(GuiView & lv) { return new GuiSpellchecker(lv); } - } // namespace frontend } // namespace lyx