X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2Ffrontends%2Fqt4%2FGuiSpellchecker.cpp;h=537d9d79b50184b8e652d75aac0ab3369db54e2e;hb=b7f6b979d0f889f08e735f35378bb20ba3788b4b;hp=3cd6ca097fbe26460ee1d8cd7899aa42ca6a0f78;hpb=ef5f53017a28025cfa972166bb371530af570e6b;p=lyx.git diff --git a/src/frontends/qt4/GuiSpellchecker.cpp b/src/frontends/qt4/GuiSpellchecker.cpp index 3cd6ca097f..537d9d79b5 100644 --- a/src/frontends/qt4/GuiSpellchecker.cpp +++ b/src/frontends/qt4/GuiSpellchecker.cpp @@ -13,6 +13,7 @@ #include #include "GuiSpellchecker.h" +#include "GuiApplication.h" #include "qt_helpers.h" @@ -23,10 +24,13 @@ #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" @@ -38,7 +42,9 @@ #include "support/lstrings.h" #include "support/textutils.h" +#include #include +#include #include "SpellChecker.h" @@ -51,46 +57,129 @@ namespace lyx { namespace frontend { -struct GuiSpellchecker::Private +struct SpellcheckerWidget::Private { - Private() : progress_(0), count_(0) {} + 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_; - /// values for progress - int total_; - int progress_; - /// word count - int count_; + /// 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_; }; -GuiSpellchecker::GuiSpellchecker(GuiView & lv) - : DockView(lv, "spellchecker", qt_("Spellchecker"), - Qt::RightDockWidgetArea), d(new GuiSpellchecker::Private) +SpellcheckerWidget::SpellcheckerWidget(GuiView * gv, DockView * dv, QWidget * parent) + : QTabWidget(parent), d(new Private(this, dv, gv)) { d->ui.setupUi(this); connect(d->ui.suggestionsLW, SIGNAL(itemDoubleClicked(QListWidgetItem*)), this, SLOT(on_replacePB_clicked())); + // 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); + + d->ui.suggestionsLW->installEventFilter(this); } -GuiSpellchecker::~GuiSpellchecker() +SpellcheckerWidget::~SpellcheckerWidget() { delete d; } -void GuiSpellchecker::on_closePB_clicked() +bool SpellcheckerWidget::eventFilter(QObject *obj, QEvent *event) { - close(); + 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 GuiSpellchecker::on_suggestionsLW_changed(QListWidgetItem * item) +void SpellcheckerWidget::on_suggestionsLW_itemClicked(QListWidgetItem * item) { if (d->ui.replaceCO->count() != 0) d->ui.replaceCO->setItemText(0, item->text()); @@ -101,7 +190,7 @@ void GuiSpellchecker::on_suggestionsLW_changed(QListWidgetItem * item) } -void GuiSpellchecker::on_replaceC0_highlighted(const QString & str) +void SpellcheckerWidget::on_replaceCO_highlighted(const QString & str) { QListWidget * lw = d->ui.suggestionsLW; if (lw->currentItem() && lw->currentItem()->text() == str) @@ -116,155 +205,423 @@ void GuiSpellchecker::on_replaceC0_highlighted(const QString & str) } -void GuiSpellchecker::updateView() +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() +{ + 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 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::on_replaceAllPB_clicked() +bool SpellcheckerWidget::initialiseParams(std::string const &) { - /// replace all occurances of word - theSpellChecker()->accept(d->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 GuiSpellchecker::on_addPB_clicked() +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_); - check(); + d->forward(); + d->check(); + d->canCheck(); } -void GuiSpellchecker::on_ignorePB_clicked() +void SpellcheckerWidget::on_ignorePB_clicked() { - check(); + /// ignore this occurrence of word + if (d->disabled()) + return; + LYXERR(Debug::GUI, "Spellchecker: ignore button"); + d->forward(); + d->check(); + d->canCheck(); } -void GuiSpellchecker::on_replacePB_clicked() +void SpellcheckerWidget::on_findNextPB_clicked() { + 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 SpellcheckerWidget::on_replacePB_clicked() +{ + 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 << ")"); - 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 - --d->count_; - check(); + dispatch(FuncRequest(LFUN_WORD_REPLACE, datastring)); + d->forward(); + d->check(); + d->canCheck(); } -void GuiSpellchecker::updateSuggestions(docstring_list & words) +void SpellcheckerWidget::on_replaceAllPB_clicked() { - QString const suggestion = toqstr(d->word_.word()); - d->ui.wordED->setText(suggestion); - QListWidget * lw = d->ui.suggestionsLW; + 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 SpellcheckerWidget::Private::updateSuggestions(docstring_list & words) +{ + QString const suggestion = toqstr(word_.word()); + ui.wordED->setText(suggestion); + QListWidget * lw = ui.suggestionsLW; lw->clear(); if (words.empty()) { - on_suggestionsLW_changed(new QListWidgetItem(suggestion)); + p->on_suggestionsLW_itemClicked(new QListWidgetItem(suggestion)); return; } for (size_t i = 0; i != words.size(); ++i) lw->addItem(toqstr(words[i])); - on_suggestionsLW_changed(lw->item(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(); - d->progress_ = countWords(begin, cur); - d->total_ = d->progress_ + countWords(cur, doc_iterator_end(&buffer())); - d->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(); + + 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 = bufferview()->cursor(); - DocIterator to; + 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_); - close(); return; } throw message; } - LYXERR(Debug::GUI, "Found word \"" << word_lang.word() << "\""); - d->count_ += progress; - d->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(); - d->word_ = word_lang; + word_ = word_lang; - int const progress_bar = d->total_ - ? int(100.0 * float(d->progress_)/d->total_) : 100; - LYXERR(Debug::GUI, "Updating spell progress."); - // set progress bar - d->ui.spellcheckPR->setValue(progress_bar); // set suggestions updateSuggestions(suggestions); + // set language + if (!word_lang.lang()) + return; + setLanguage(word_lang.lang()); + // mark misspelled word + setSelection(from, to); +} - // 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); + +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_); } -void GuiSpellchecker::showSummary() +GuiSpellchecker::~GuiSpellchecker() { - if (d->count_ == 0) { - close(); - return; - } + setFocusProxy(0); + delete widget_; +} - docstring message; - if (d->count_ != 1) - message = bformat(_("%1$d words checked."), d->count_); - else - message = _("One word checked."); - close(); - Alert::information(_("Spelling check completed"), message); +void GuiSpellchecker::updateView() +{ + widget_->updateView(); } -Dialog * createGuiSpellchecker(GuiView & lv) { return new GuiSpellchecker(lv); } +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