X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2FBufferView.C;h=008f03e4aa11e10347289ed7911525d6fbb5aa85;hb=3f8fa07c6ed38218d6a4048f2993a512aa942c96;hp=677abdad0ab2d4c222409fcd6a1e94597059c29a;hpb=b1a87cc7781bd11d9481b69e96eb67b2d637e167;p=lyx.git diff --git a/src/BufferView.C b/src/BufferView.C index 677abdad0a..008f03e4aa 100644 --- a/src/BufferView.C +++ b/src/BufferView.C @@ -17,262 +17,1158 @@ #include "BufferView.h" #include "buffer.h" +#include "buffer_funcs.h" #include "bufferlist.h" #include "bufferparams.h" -#include "BufferView_pimpl.h" -#include "CutAndPaste.h" +#include "bufferview_funcs.h" #include "coordcache.h" +#include "CutAndPaste.h" #include "debug.h" +#include "dispatchresult.h" +#include "errorlist.h" +#include "factory.h" +#include "FloatList.h" #include "funcrequest.h" #include "FuncStatus.h" #include "gettext.h" +#include "intl.h" #include "insetiterator.h" #include "language.h" +#include "LaTeXFeatures.h" +#include "lyx_cb.h" // added for Dispatch functions +#include "lyx_main.h" +#include "lyxfind.h" +#include "lyxfunc.h" #include "lyxlayout.h" #include "lyxtext.h" #include "lyxtextclass.h" +#include "lyxrc.h" +#include "session.h" #include "paragraph.h" #include "paragraph_funcs.h" +#include "ParagraphParameters.h" #include "pariterator.h" #include "texrow.h" +#include "toc.h" #include "undo.h" +#include "vspace.h" #include "WordLangTuple.h" +#include "metricsinfo.h" -#include "frontends/Alert.h" -#include "frontends/Clipboard.h" -#include "frontends/Dialogs.h" -#include "frontends/LyXView.h" -#include "frontends/Gui.h" - +#include "insets/insetbibtex.h" #include "insets/insetcommand.h" // ChangeRefs +#include "insets/insetref.h" #include "insets/insettext.h" +#include "frontends/Alert.h" +#include "frontends/FileDialog.h" +#include "frontends/FontMetrics.h" +#include "frontends/Selection.h" + +#include "graphics/Previews.h" + +#include "support/convert.h" +#include "support/filefilterlist.h" +#include "support/filetools.h" +#include "support/package.h" +#include "support/types.h" -using lyx::support::bformat; +#include +#include -using lyx::cap::setSelectionRange; +#include +#include + + +namespace lyx { + +using support::addPath; +using support::bformat; +using support::FileFilterList; +using support::FileName; +using support::fileSearch; +using support::isDirWriteable; +using support::isFileReadable; +using support::makeDisplayPath; +using support::package; using std::distance; -using std::find; +using std::endl; +using std::istringstream; +using std::make_pair; +using std::min; +using std::max; +using std::mem_fun_ref; using std::string; using std::vector; +namespace Alert = frontend::Alert; -extern BufferList bufferlist; +namespace { +/// Return an inset of this class if it exists at the current cursor position +template +T * getInsetByCode(LCursor & cur, InsetBase::Code code) +{ + T * inset = 0; + DocIterator it = cur; + if (it.nextInset() && + it.nextInset()->lyxCode() == code) { + inset = static_cast(it.nextInset()); + } + return inset; +} -BufferView::BufferView(LyXView * owner) - : pimpl_(new Pimpl(*this, owner)) -{} +} // anon namespace -BufferView::~BufferView() +BufferView::BufferView() + : width_(0), height_(0), buffer_(0), wh_(0), + cursor_(*this), + multiparsel_cache_(false), anchor_ref_(0), offset_ref_(0), + intl_(new Intl), last_inset_(0) { - delete pimpl_; + xsel_cache_.set = false; + intl_->initKeyMapper(lyxrc.use_kbmap); } -void BufferView::unsetXSel() +BufferView::~BufferView() { - pimpl_->xsel_cache_.set = false; } Buffer * BufferView::buffer() const { - return pimpl_->buffer_; + return buffer_; } -LyXView * BufferView::owner() const +void BufferView::setBuffer(Buffer * b) { - return pimpl_->owner_; -} + lyxerr[Debug::INFO] << BOOST_CURRENT_FUNCTION + << "[ b = " << b << "]" << endl; + + if (buffer_) { + // Save the actual cursor position and anchor inside the + // buffer so that it can be restored in case we rechange + // to this buffer later on. + buffer_->saveCursor(cursor_.selectionBegin(), + cursor_.selectionEnd()); + // current buffer is going to be switched-off, save cursor pos + LyX::ref().session().lastFilePos().save(FileName(buffer_->fileName()), + boost::tie(cursor_.pit(), cursor_.pos()) ); + } + // If we're quitting lyx, don't bother updating stuff + if (quitting) { + buffer_ = 0; + return; + } -lyx::frontend::Painter & BufferView::painter() const -{ - return pimpl_->painter(); -} + // If we are closing current buffer, switch to the first in + // buffer list. + if (!b) { + lyxerr[Debug::INFO] << BOOST_CURRENT_FUNCTION + << " No Buffer!" << endl; + // We are closing the buffer, use the first buffer as current + buffer_ = theBufferList().first(); + } else { + // Set current buffer + buffer_ = b; + } + + // Reset old cursor + cursor_ = LCursor(*this); + anchor_ref_ = 0; + offset_ref_ = 0; + + if (buffer_) { + lyxerr[Debug::INFO] << BOOST_CURRENT_FUNCTION + << "Buffer addr: " << buffer_ << endl; + cursor_.push(buffer_->inset()); + cursor_.resetAnchor(); + buffer_->text().setCurrentFont(cursor_); + if (buffer_->getCursor().size() > 0 && + buffer_->getAnchor().size() > 0) + { + cursor_.setCursor(buffer_->getAnchor().asDocIterator(&(buffer_->inset()))); + cursor_.resetAnchor(); + cursor_.setCursor(buffer_->getCursor().asDocIterator(&(buffer_->inset()))); + cursor_.setSelection(); + theSelection().haveSelection(cursor_.selection()); + } + } + if (buffer_) + updateMetrics(false); -void BufferView::setBuffer(Buffer * b) -{ - pimpl_->setBuffer(b); + if (buffer_ && graphics::Previews::status() != LyXRC::PREVIEW_OFF) + graphics::Previews::get().generateBufferPreviews(*buffer_); } -void BufferView::newFile(string const & fn, string const & tn, bool named) +bool BufferView::loadLyXFile(FileName const & filename, bool tolastfiles) { - pimpl_->newFile(fn, tn, named); -} + // File already open? + if (theBufferList().exists(filename.absFilename())) { + docstring const file = makeDisplayPath(filename.absFilename(), 20); + docstring text = bformat(_("The document %1$s is already " + "loaded.\n\nDo you want to revert " + "to the saved version?"), file); + int const ret = Alert::prompt(_("Revert to saved document?"), + text, 0, 1, _("&Revert"), _("&Switch to document")); + + if (ret != 0) { + setBuffer(theBufferList().getBuffer(filename.absFilename())); + return true; + } + // FIXME: should be LFUN_REVERT + if (!theBufferList().close(theBufferList().getBuffer(filename.absFilename()), false)) + return false; + // Fall through to new load. (Asger) + buffer_ = 0; + } + Buffer * b = 0; -bool BufferView::loadLyXFile(string const & fn, bool tl) -{ - return pimpl_->loadLyXFile(fn, tl); -} + if (isFileReadable(filename)) { + b = theBufferList().newBuffer(filename.absFilename()); + if (!lyx::loadLyXFile(b, filename)) { + theBufferList().release(b); + return false; + } + } else { + docstring text = bformat(_("The document %1$s does not yet " + "exist.\n\nDo you want to create " + "a new document?"), from_utf8(filename.absFilename())); + int const ret = Alert::prompt(_("Create new document?"), + text, 0, 1, _("&Create"), _("Cancel")); + + if (ret == 0) { + b = newFile(filename.absFilename(), string(), true); + if (!b) + return false; + } else + return false; + } + setBuffer(b); + // Send the "errors" signal in case of parsing errors + b->errors("Parse"); + + // Update the labels and section numbering. + updateLabels(*buffer_); + // scroll to the position when the file was last closed + if (lyxrc.use_lastfilepos) { + pit_type pit; + pos_type pos; + boost::tie(pit, pos) = LyX::ref().session().lastFilePos().load(filename); + // if successfully move to pit (returned par_id is not zero), update metrics + if (moveToPosition(pit, 0, pos).get<1>()) { + if (fitCursor()) + updateMetrics(false); + } + } -void BufferView::reload() -{ - string const fn = buffer()->fileName(); - if (bufferlist.close(buffer(), false)) - loadLyXFile(fn); + if (tolastfiles) + LyX::ref().session().lastFiles().add(FileName(b->fileName())); + + return true; } void BufferView::resize() { - if (pimpl_->buffer_) - pimpl_->resizeCurrentBuffer(); + if (!buffer_) + return; + + lyxerr[Debug::DEBUG] << BOOST_CURRENT_FUNCTION << endl; + + updateMetrics(false); + switchKeyMap(); } bool BufferView::fitCursor() { - return pimpl_->fitCursor(); + if (bv_funcs::status(this, cursor_) == bv_funcs::CUR_INSIDE) { + frontend::FontMetrics const & fm = + theFontMetrics(cursor_.getFont()); + int const asc = fm.maxAscent(); + int const des = fm.maxDescent(); + Point const p = bv_funcs::getPos(*this, cursor_, cursor_.boundary()); + if (p.y_ - asc >= 0 && p.y_ + des < height_) + return false; + } + center(); + return true; } -void BufferView::update(Update::flags flags) +bool BufferView::multiParSel() { - pimpl_->update(flags); + if (!cursor_.selection()) + return false; + bool ret = multiparsel_cache_; + multiparsel_cache_ = cursor_.selBegin().pit() != cursor_.selEnd().pit(); + // Either this, or previous selection spans paragraphs + return ret || multiparsel_cache_; +} + + +bool BufferView::update(Update::flags flags) +{ + // last_inset_ points to the last visited inset. This pointer may become + // invalid because of keyboard editing. Since all such operations + // causes screen update(), I reset last_inset_ to avoid such a problem. + last_inset_ = 0; + // This is close to a hot-path. + if (lyxerr.debugging(Debug::DEBUG)) { + lyxerr[Debug::DEBUG] + << BOOST_CURRENT_FUNCTION + << "[fitcursor = " << (flags & Update::FitCursor) + << ", forceupdate = " << (flags & Update::Force) + << ", singlepar = " << (flags & Update::SinglePar) + << "] buffer: " << buffer_ << endl; + } + + // Check needed to survive LyX startup + if (!buffer_) + return false; + + if (lyxerr.debugging(Debug::WORKAREA)) { + lyxerr[Debug::WORKAREA] << "BufferView::update" << std::endl; + } + + // Update macro store + buffer_->buildMacros(); + + // Now do the first drawing step if needed. This consists on updating + // the CoordCache in updateMetrics(). + // The second drawing step is done in WorkArea::redraw() if needed. + + // Case when no explicit update is requested. + if (!flags) { + // no need to redraw anything. + metrics_info_.update_strategy = NoScreenUpdate; + return false; + } + + if (flags == Update::Decoration) { + metrics_info_.update_strategy = DecorationUpdate; + return true; + } + + if (flags == Update::FitCursor + || flags == (Update::Decoration | Update::FitCursor)) { + bool const fit_cursor = fitCursor(); + // tell the frontend to update the screen if needed. + if (fit_cursor) { + updateMetrics(false); + return true; + } + if (flags & Update::Decoration) { + metrics_info_.update_strategy = DecorationUpdate; + return true; + } + // no screen update is needed. + metrics_info_.update_strategy = NoScreenUpdate; + return false; + } + + bool full_metrics = flags & Update::Force; + if (flags & Update::MultiParSel) + full_metrics |= multiParSel(); + + bool const single_par = !full_metrics; + updateMetrics(single_par); + + if (flags & Update::FitCursor && fitCursor()) + updateMetrics(false); + + // tell the frontend to update the screen. + return true; } void BufferView::updateScrollbar() { - pimpl_->updateScrollbar(); + if (!buffer_) { + lyxerr[Debug::DEBUG] << BOOST_CURRENT_FUNCTION + << " no text in updateScrollbar" << endl; + scrollbarParameters_.reset(); + return; + } + + LyXText & t = buffer_->text(); + TextMetrics & tm = text_metrics_[&t]; + + int const parsize = int(t.paragraphs().size() - 1); + if (anchor_ref_ > parsize) { + anchor_ref_ = parsize; + offset_ref_ = 0; + } + + if (lyxerr.debugging(Debug::GUI)) { + lyxerr[Debug::GUI] + << BOOST_CURRENT_FUNCTION + << " Updating scrollbar: height: " << t.paragraphs().size() + << " curr par: " << cursor_.bottom().pit() + << " default height " << defaultRowHeight() << endl; + } + + // It would be better to fix the scrollbar to understand + // values in [0..1] and divide everything by wh + + // estimated average paragraph height: + if (wh_ == 0) + wh_ = height_ / 4; + + int h = tm.parMetrics(anchor_ref_).height(); + + // Normalize anchor/offset (MV): + while (offset_ref_ > h && anchor_ref_ < parsize) { + anchor_ref_++; + offset_ref_ -= h; + h = tm.parMetrics(anchor_ref_).height(); + } + // Look at paragraph heights on-screen + int sumh = 0; + int nh = 0; + for (pit_type pit = anchor_ref_; pit <= parsize; ++pit) { + if (sumh > height_) + break; + int const h2 = tm.parMetrics(pit).height(); + sumh += h2; + nh++; + } + + BOOST_ASSERT(nh); + int const hav = sumh / nh; + // More realistic average paragraph height + if (hav > wh_) + wh_ = hav; + + BOOST_ASSERT(h); + scrollbarParameters_.height = (parsize + 1) * wh_; + scrollbarParameters_.position = anchor_ref_ * wh_ + int(offset_ref_ * wh_ / float(h)); + scrollbarParameters_.lineScrollHeight = int(wh_ * defaultRowHeight() / float(h)); } ScrollbarParameters const & BufferView::scrollbarParameters() const { - return pimpl_->scrollbarParameters(); + return scrollbarParameters_; } void BufferView::scrollDocView(int value) { - pimpl_->scrollDocView(value); -} + lyxerr[Debug::GUI] << BOOST_CURRENT_FUNCTION + << "[ value = " << value << "]" << endl; + if (!buffer_) + return; -bool BufferView::available() const -{ - return pimpl_->available(); -} + LyXText & t = buffer_->text(); + TextMetrics & tm = text_metrics_[&t]; + float const bar = value / float(wh_ * t.paragraphs().size()); -Change const BufferView::getCurrentChange() -{ - return pimpl_->getCurrentChange(); + anchor_ref_ = int(bar * t.paragraphs().size()); + if (anchor_ref_ > int(t.paragraphs().size()) - 1) + anchor_ref_ = int(t.paragraphs().size()) - 1; + + tm.redoParagraph(anchor_ref_); + int const h = tm.parMetrics(anchor_ref_).height(); + offset_ref_ = int((bar * t.paragraphs().size() - anchor_ref_) * h); + updateMetrics(false); } -void BufferView::savePosition(unsigned int i) +void BufferView::setCursorFromScrollbar() { - pimpl_->savePosition(i); + LyXText & t = buffer_->text(); + + int const height = 2 * defaultRowHeight(); + int const first = height; + int const last = height_ - height; + LCursor & cur = cursor_; + + bv_funcs::CurStatus st = bv_funcs::status(this, cur); + + switch (st) { + case bv_funcs::CUR_ABOVE: + t.setCursorFromCoordinates(cur, 0, first); + cur.clearSelection(); + break; + case bv_funcs::CUR_BELOW: + t.setCursorFromCoordinates(cur, 0, last); + cur.clearSelection(); + break; + case bv_funcs::CUR_INSIDE: + int const y = bv_funcs::getPos(*this, cur, cur.boundary()).y_; + int const newy = min(last, max(y, first)); + if (y != newy) { + cur.reset(buffer_->inset()); + t.setCursorFromCoordinates(cur, 0, newy); + } + } } -void BufferView::restorePosition(unsigned int i) +Change const BufferView::getCurrentChange() const { - pimpl_->restorePosition(i); + if (!cursor_.selection()) + return Change(Change::UNCHANGED); + + DocIterator dit = cursor_.selectionBegin(); + return dit.paragraph().lookupChange(dit.pos()); } -bool BufferView::isSavedPosition(unsigned int i) +void BufferView::saveBookmark(bool persistent) { - return pimpl_->isSavedPosition(i); + LyX::ref().session().bookmarks().save( + FileName(buffer_->fileName()), + cursor_.pit(), + cursor_.paragraph().id(), + cursor_.pos(), + persistent + ); + if (persistent) + // emit message signal. + message(_("Save bookmark")); } -void BufferView::saveSavedPositions() + +boost::tuple BufferView::moveToPosition(pit_type par_pit, int par_id, pos_type par_pos) { - return pimpl_->saveSavedPositions(); + cursor_.clearSelection(); + + // if a valid par_id is given, try it first + if (par_id > 0) { + ParIterator par = buffer_->getParFromID(par_id); + if (par != buffer_->par_iterator_end()) { + setCursor(makeDocIterator(par, min(par->size(), par_pos))); + return boost::make_tuple(cursor_.pit(), par_id); + } + } + // if par_id == 0, or searching through par_id failed + if (static_cast(par_pit) < buffer_->paragraphs().size()) { + ParIterator it = buffer_->par_iterator_begin(); + ParIterator const end = buffer_->par_iterator_end(); + for (; it != end; ++it) + if (it.pit() == par_pit) { + // restored pos may be bigger than it->size + setCursor(makeDocIterator(it, min(par_pos, it->size()))); + return boost::make_tuple(par_pit, it->id()); + } + } + // both methods fail + return boost::make_tuple(pit_type(0), 0); } + void BufferView::switchKeyMap() { - pimpl_->switchKeyMap(); + if (!lyxrc.rtl_support) + return; + + if (getLyXText()->real_current_font.isRightToLeft()) { + if (intl_->keymap == Intl::PRIMARY) + intl_->keyMapSec(); + } else { + if (intl_->keymap == Intl::SECONDARY) + intl_->keyMapPrim(); + } } int BufferView::workWidth() const { - return pimpl_->width(); + return width_; } void BufferView::center() { - pimpl_->center(); + CursorSlice & bot = cursor_.bottom(); + TextMetrics & tm = text_metrics_[bot.text()]; + pit_type const pit = bot.pit(); + tm.redoParagraph(pit); + ParagraphMetrics const & pm = tm.parMetrics(pit); + anchor_ref_ = pit; + offset_ref_ = bv_funcs::coordOffset(*this, cursor_, cursor_.boundary()).y_ + + pm.ascent() - height_ / 2; } FuncStatus BufferView::getStatus(FuncRequest const & cmd) { - return pimpl_->getStatus(cmd); + FuncStatus flag; + + switch (cmd.action) { + + case LFUN_UNDO: + flag.enabled(!buffer_->undostack().empty()); + break; + case LFUN_REDO: + flag.enabled(!buffer_->redostack().empty()); + break; + case LFUN_FILE_INSERT: + case LFUN_FILE_INSERT_PLAINTEXT_PARA: + case LFUN_FILE_INSERT_PLAINTEXT: + case LFUN_BOOKMARK_SAVE: + // FIXME: Actually, these LFUNS should be moved to LyXText + flag.enabled(cursor_.inTexted()); + break; + case LFUN_FONT_STATE: + case LFUN_LABEL_INSERT: + case LFUN_PARAGRAPH_GOTO: + // FIXME handle non-trivially + case LFUN_OUTLINE_UP: + case LFUN_OUTLINE_DOWN: + case LFUN_OUTLINE_IN: + case LFUN_OUTLINE_OUT: + case LFUN_NOTE_NEXT: + case LFUN_REFERENCE_NEXT: + case LFUN_WORD_FIND: + case LFUN_WORD_REPLACE: + case LFUN_MARK_OFF: + case LFUN_MARK_ON: + case LFUN_MARK_TOGGLE: + case LFUN_SCREEN_RECENTER: + case LFUN_BIBTEX_DATABASE_ADD: + case LFUN_BIBTEX_DATABASE_DEL: + case LFUN_WORDS_COUNT: + case LFUN_NEXT_INSET_TOGGLE: + flag.enabled(true); + break; + + case LFUN_LABEL_GOTO: { + flag.enabled(!cmd.argument().empty() + || getInsetByCode(cursor_, InsetBase::REF_CODE)); + break; + } + + case LFUN_CHANGES_TRACK: + flag.enabled(true); + flag.setOnOff(buffer_->params().trackChanges); + break; + + case LFUN_CHANGES_OUTPUT: { + OutputParams runparams; + LaTeXFeatures features(*buffer_, buffer_->params(), runparams); + flag.enabled(buffer_ && features.isAvailable("dvipost")); + flag.setOnOff(buffer_->params().outputChanges); + break; + } + + case LFUN_CHANGES_MERGE: + case LFUN_CHANGE_NEXT: + case LFUN_ALL_CHANGES_ACCEPT: + case LFUN_ALL_CHANGES_REJECT: + // TODO: context-sensitive enabling of LFUNs + // In principle, these command should only be enabled if there + // is a change in the document. However, without proper + // optimizations, this will inevitably result in poor performance. + flag.enabled(buffer_); + break; + + case LFUN_BUFFER_TOGGLE_COMPRESSION: { + flag.setOnOff(buffer_->params().compressed); + break; + } + + default: + flag.enabled(false); + } + + return flag; } -bool BufferView::dispatch(FuncRequest const & ev) +bool BufferView::dispatch(FuncRequest const & cmd) { - return pimpl_->dispatch(ev); + //lyxerr << BOOST_CURRENT_FUNCTION + // << [ cmd = " << cmd << "]" << endl; + + // Make sure that the cached BufferView is correct. + lyxerr[Debug::ACTION] << BOOST_CURRENT_FUNCTION + << " action[" << cmd.action << ']' + << " arg[" << to_utf8(cmd.argument()) << ']' + << " x[" << cmd.x << ']' + << " y[" << cmd.y << ']' + << " button[" << cmd.button() << ']' + << endl; + + LCursor & cur = cursor_; + + switch (cmd.action) { + + case LFUN_UNDO: + if (buffer_) { + cur.message(_("Undo")); + cur.clearSelection(); + if (!textUndo(*this)) + cur.message(_("No further undo information")); + update(); + switchKeyMap(); + } + break; + + case LFUN_REDO: + if (buffer_) { + cur.message(_("Redo")); + cur.clearSelection(); + if (!textRedo(*this)) + cur.message(_("No further redo information")); + update(); + switchKeyMap(); + } + break; + + case LFUN_FILE_INSERT: + // FIXME UNICODE + menuInsertLyXFile(to_utf8(cmd.argument())); + break; + + case LFUN_FILE_INSERT_PLAINTEXT_PARA: + // FIXME UNICODE + insertPlaintextFile(this, to_utf8(cmd.argument()), true); + break; + + case LFUN_FILE_INSERT_PLAINTEXT: + // FIXME UNICODE + insertPlaintextFile(this, to_utf8(cmd.argument()), false); + break; + + case LFUN_FONT_STATE: + cur.message(cur.currentState()); + break; + + case LFUN_BOOKMARK_SAVE: + saveBookmark(convert(to_utf8(cmd.argument()))); + break; + + case LFUN_LABEL_GOTO: { + docstring label = cmd.argument(); + if (label.empty()) { + InsetRef * inset = + getInsetByCode(cursor_, + InsetBase::REF_CODE); + if (inset) { + label = inset->getParam("reference"); + // persistent=false: use temp_bookmark + saveBookmark(false); + } + } + + if (!label.empty()) + gotoLabel(label); + break; + } + + case LFUN_PARAGRAPH_GOTO: { + int const id = convert(to_utf8(cmd.argument())); + int i = 0; + for (Buffer * b = buffer_; i == 0 || b != buffer_; b = theBufferList().next(b)) { + ParIterator par = b->getParFromID(id); + if (par == b->par_iterator_end()) { + lyxerr[Debug::INFO] + << "No matching paragraph found! [" + << id << "]." << endl; + } else { + lyxerr[Debug::INFO] + << "Paragraph " << par->id() + << " found in buffer `" + << b->fileName() << "'." << endl; + + if (b == buffer_) { + // Set the cursor + setCursor(makeDocIterator(par, 0)); + update(); + switchKeyMap(); + } else { + // Switch to other buffer view and resend cmd + theLyXFunc().dispatch(FuncRequest( + LFUN_BUFFER_SWITCH, b->fileName())); + theLyXFunc().dispatch(cmd); + } + break; + } + ++i; + } + break; + } + + case LFUN_OUTLINE_UP: + toc::outline(toc::Up, cursor_); + cursor_.text()->setCursor(cursor_, cursor_.pit(), 0); + updateLabels(*buffer_); + break; + case LFUN_OUTLINE_DOWN: + toc::outline(toc::Down, cursor_); + cursor_.text()->setCursor(cursor_, cursor_.pit(), 0); + updateLabels(*buffer_); + break; + case LFUN_OUTLINE_IN: + toc::outline(toc::In, cursor_); + updateLabels(*buffer_); + break; + case LFUN_OUTLINE_OUT: + toc::outline(toc::Out, cursor_); + updateLabels(*buffer_); + break; + + case LFUN_NOTE_NEXT: + bv_funcs::gotoInset(this, InsetBase::NOTE_CODE, false); + break; + + case LFUN_REFERENCE_NEXT: { + vector tmp; + tmp.push_back(InsetBase::LABEL_CODE); + tmp.push_back(InsetBase::REF_CODE); + bv_funcs::gotoInset(this, tmp, true); + break; + } + + case LFUN_CHANGES_TRACK: + buffer_->params().trackChanges = !buffer_->params().trackChanges; + break; + + case LFUN_CHANGES_OUTPUT: { + buffer_->params().outputChanges = !buffer_->params().outputChanges; + break; + } + + case LFUN_CHANGE_NEXT: + findNextChange(this); + break; + + case LFUN_CHANGES_MERGE: + if (findNextChange(this)) + showDialog("changes"); + break; + + case LFUN_ALL_CHANGES_ACCEPT: { + cursor_.reset(buffer_->inset()); +#ifdef WITH_WARNINGS +#warning FIXME changes +#endif + while (findNextChange(this)) + getLyXText()->acceptOrRejectChange(cursor_, true); + update(); + break; + } + + case LFUN_ALL_CHANGES_REJECT: { + cursor_.reset(buffer_->inset()); +#ifdef WITH_WARNINGS +#warning FIXME changes +#endif + while (findNextChange(this)) + getLyXText()->acceptOrRejectChange(cursor_, false); + break; + } + + case LFUN_WORD_FIND: + find(this, cmd); + break; + + case LFUN_WORD_REPLACE: + replace(this, cmd); + break; + + case LFUN_MARK_OFF: + cur.clearSelection(); + cur.resetAnchor(); + cur.message(from_utf8(N_("Mark off"))); + break; + + case LFUN_MARK_ON: + cur.clearSelection(); + cur.mark() = true; + cur.resetAnchor(); + cur.message(from_utf8(N_("Mark on"))); + break; + + case LFUN_MARK_TOGGLE: + cur.clearSelection(); + if (cur.mark()) { + cur.mark() = false; + cur.message(from_utf8(N_("Mark removed"))); + } else { + cur.mark() = true; + cur.message(from_utf8(N_("Mark set"))); + } + cur.resetAnchor(); + break; + + case LFUN_SCREEN_RECENTER: + center(); + break; + + case LFUN_BIBTEX_DATABASE_ADD: { + LCursor tmpcur = cursor_; + bv_funcs::findInset(tmpcur, InsetBase::BIBTEX_CODE, false); + InsetBibtex * inset = getInsetByCode(tmpcur, + InsetBase::BIBTEX_CODE); + if (inset) { + if (inset->addDatabase(to_utf8(cmd.argument()))) + buffer_->updateBibfilesCache(); + } + break; + } + + case LFUN_BIBTEX_DATABASE_DEL: { + LCursor tmpcur = cursor_; + bv_funcs::findInset(tmpcur, InsetBase::BIBTEX_CODE, false); + InsetBibtex * inset = getInsetByCode(tmpcur, + InsetBase::BIBTEX_CODE); + if (inset) { + if (inset->delDatabase(to_utf8(cmd.argument()))) + buffer_->updateBibfilesCache(); + } + break; + } + + case LFUN_WORDS_COUNT: { + DocIterator from, to; + if (cur.selection()) { + from = cur.selectionBegin(); + to = cur.selectionEnd(); + } else { + from = doc_iterator_begin(buffer_->inset()); + to = doc_iterator_end(buffer_->inset()); + } + int const count = countWords(from, to); + docstring message; + if (count != 1) { + if (cur.selection()) + message = bformat(_("%1$d words in selection."), + count); + else + message = bformat(_("%1$d words in document."), + count); + } + else { + if (cur.selection()) + message = _("One word in selection."); + else + message = _("One word in document."); + } + + Alert::information(_("Count words"), message); + } + break; + + case LFUN_BUFFER_TOGGLE_COMPRESSION: + // turn compression on/off + buffer_->params().compressed = !buffer_->params().compressed; + break; + + case LFUN_NEXT_INSET_TOGGLE: { + // this is the real function we want to invoke + FuncRequest tmpcmd = FuncRequest(LFUN_INSET_TOGGLE, cmd.origin); + // if there is an inset at cursor, see whether it + // wants to toggle. + InsetBase * inset = cur.nextInset(); + if (inset && inset->isActive()) { + LCursor tmpcur = cur; + tmpcur.pushLeft(*inset); + inset->dispatch(tmpcur, tmpcmd); + if (tmpcur.result().dispatched()) { + cur.dispatched(); + } + } + // if it did not work, try the underlying inset. + if (!cur.result().dispatched()) + cur.dispatch(tmpcmd); + + if (cur.result().dispatched()) + cur.clearSelection(); + + break; + } + + default: + return false; + } + + return true; } -void BufferView::selectionRequested() +docstring const BufferView::requestSelection() { - pimpl_->selectionRequested(); + if (!buffer_) + return docstring(); + + LCursor & cur = cursor_; + + if (!cur.selection()) { + xsel_cache_.set = false; + return docstring(); + } + + if (!xsel_cache_.set || + cur.top() != xsel_cache_.cursor || + cur.anchor_.top() != xsel_cache_.anchor) + { + xsel_cache_.cursor = cur.top(); + xsel_cache_.anchor = cur.anchor_.top(); + xsel_cache_.set = cur.selection(); + return cur.selectionAsString(false); + } + return docstring(); } -void BufferView::selectionLost() +void BufferView::clearSelection() { - pimpl_->selectionLost(); + if (buffer_) { + cursor_.clearSelection(); + xsel_cache_.set = false; + // The buffer did not really change, but this causes the + // redraw we need because we cleared the selection above. + buffer_->changed(); + } } void BufferView::workAreaResize(int width, int height) { - pimpl_->workAreaResize(width, height); -} + // Update from work area + width_ = width; + height_ = height; + // The complete text metrics will be redone. + text_metrics_.clear(); -void BufferView::workAreaKeyPress(LyXKeySymPtr key, key_modifier::state state) -{ - pimpl_->workAreaKeyPress(key, state); + if (buffer_) + resize(); } -bool BufferView::workAreaDispatch(FuncRequest const & ev) +bool BufferView::workAreaDispatch(FuncRequest const & cmd0) { - return pimpl_->workAreaDispatch(ev); -} + //lyxerr << BOOST_CURRENT_FUNCTION << "[ cmd0 " << cmd0 << "]" << endl; + + // This is only called for mouse related events including + // LFUN_FILE_OPEN generated by drag-and-drop. + FuncRequest cmd = cmd0; + + // E.g. Qt mouse press when no buffer + if (!buffer_) + return false; + + LCursor cur(*this); + cur.push(buffer_->inset()); + cur.selection() = cursor_.selection(); + + // Either the inset under the cursor or the + // surrounding LyXText will handle this event. + + // Build temporary cursor. + cmd.y = min(max(cmd.y, -1), height_); + InsetBase * inset = buffer_->text().editXY(cur, cmd.x, cmd.y); + + //lyxerr << BOOST_CURRENT_FUNCTION + // << " * hit inset at tip: " << inset << endl; + //lyxerr << BOOST_CURRENT_FUNCTION + // << " * created temp cursor:" << cur << endl; + + // NOTE: editXY returns the top level inset of nested insets. If you happen + // to move from a text (inset=0) to a text inside an inset (e.g. an opened + // footnote inset, again inset=0), that inset will not be redrawn. + if (cmd.action == LFUN_MOUSE_MOTION && cmd.button() == mouse_button::none) { + bool need_redraw = false; + + if (inset != last_inset_) { + if (last_inset_) + need_redraw |= last_inset_->setMouseHover(false); + if (inset) + need_redraw |= inset->setMouseHover(true); + last_inset_ = inset; + } + // if last metrics update was in singlepar mode, WorkArea::redraw() will + // not expose the button for redraw. We adjust here the metrics dimension + // to enable a full redraw. + // FIXME: It is possible to redraw only the area around the button! + if (need_redraw + && metrics_info_.update_strategy == SingleParUpdate) { + // FIXME: It should be possible to redraw only the area around + // the button by doing this: + // + //metrics_info_.singlepar = false; + //metrics_info_.y1 = ymin of button; + //metrics_info_.y2 = ymax of button; + // + // Unfortunately, rowpainter.C:paintText() does not distinguish + // between background updates and text updates. So we use the hammer + // solution for now. We could also avoid the updateMetrics() below + // by using the first and last pit of the CoordCache. Have a look + // at LyXText::getPitNearY() to see what I mean. + // + //metrics_info_.pit1 = first pit of CoordCache; + //metrics_info_.pit2 = last pit of CoordCache; + //metrics_info_.singlepar = false; + //metrics_info_.y1 = 0; + //metrics_info_.y2 = height_; + // + updateMetrics(false); + } -void BufferView::scroll(int lines) -{ - pimpl_->scroll(lines); -} + // This event (moving without mouse click) is not passed further. + // This should be changed if it is further utilized. + return need_redraw; + } + // Put anchor at the same position. + cur.resetAnchor(); -void BufferView::showErrorList(string const & action) const -{ - if (getErrorList().size()) { - string const title = bformat(_("%1$s Errors (%2$s)"), - action, buffer()->fileName()); - owner()->getDialogs().show("errorlist", title); - pimpl_->errorlist_.clear(); + // Try to dispatch to an non-editable inset near this position + // via the temp cursor. If the inset wishes to change the real + // cursor it has to do so explicitly by using + // cur.bv().cursor() = cur; (or similar) + if (inset) { + inset->dispatch(cur, cmd); } + + // Now dispatch to the temporary cursor. If the real cursor should + // be modified, the inset's dispatch has to do so explicitly. + if (!cur.result().dispatched()) + cur.dispatch(cmd); + + // Redraw if requested and necessary. + if (cur.result().dispatched() && cur.result().update()) + return update(cur.result().update()); + + return false; } -ErrorList const & BufferView::getErrorList() const +void BufferView::scroll(int /*lines*/) { - return pimpl_->errorlist_; +// if (!buffer_) +// return; +// +// LyXText const * t = &buffer_->text(); +// int const line_height = defaultRowHeight(); +// +// // The new absolute coordinate +// int new_top_y = top_y() + lines * line_height; +// +// // Restrict to a valid value +// new_top_y = std::min(t->height() - 4 * line_height, new_top_y); +// new_top_y = std::max(0, new_top_y); +// +// scrollDocView(new_top_y); +// } @@ -281,21 +1177,21 @@ void BufferView::setCursorFromRow(int row) int tmpid = -1; int tmppos = -1; - buffer()->texrow().getIdFromRow(row, tmpid, tmppos); + buffer_->texrow().getIdFromRow(row, tmpid, tmppos); if (tmpid == -1) - text()->setCursor(cursor(), 0, 0); + buffer_->text().setCursor(cursor_, 0, 0); else - text()->setCursor(cursor(), buffer()->getParFromID(tmpid).pit(), tmppos); + buffer_->text().setCursor(cursor_, buffer_->getParFromID(tmpid).pit(), tmppos); } -void BufferView::gotoLabel(string const & label) +void BufferView::gotoLabel(docstring const & label) { - for (InsetIterator it = inset_iterator_begin(buffer()->inset()); it; ++it) { - vector labels; - it->getLabelList(*buffer(), labels); - if (find(labels.begin(),labels.end(),label) != labels.end()) { + for (InsetIterator it = inset_iterator_begin(buffer_->inset()); it; ++it) { + vector labels; + it->getLabelList(*buffer_, labels); + if (std::find(labels.begin(), labels.end(), label) != labels.end()) { setCursor(it); update(); return; @@ -304,14 +1200,9 @@ void BufferView::gotoLabel(string const & label) } -void BufferView::hideCursor() -{ - pimpl_->gui().guiCursor().hide(); -} - LyXText * BufferView::getLyXText() { - LyXText * text = cursor().innerText(); + LyXText * text = cursor_.innerText(); BOOST_ASSERT(text); return text; } @@ -319,21 +1210,39 @@ LyXText * BufferView::getLyXText() LyXText const * BufferView::getLyXText() const { - LyXText const * text = cursor().innerText(); + LyXText const * text = cursor_.innerText(); BOOST_ASSERT(text); return text; } -int BufferView::workHeight() const +TextMetrics const & BufferView::textMetrics(LyXText const * t) const +{ + return const_cast(this)->textMetrics(t); +} + + +TextMetrics & BufferView::textMetrics(LyXText const * t) +{ + TextMetricsCache::iterator tmc_it = text_metrics_.find(t); + if (tmc_it == text_metrics_.end()) { + tmc_it = text_metrics_.insert( + make_pair(t, TextMetrics(this, const_cast(t)))).first; + } + return tmc_it->second; +} + + +ParagraphMetrics const & BufferView::parMetrics(LyXText const * t, + pit_type pit) const { - return pimpl_->height(); + return textMetrics(t).parMetrics(pit); } -LyXText * BufferView::text() const +int BufferView::workHeight() const { - return buffer() ? &buffer()->text() : 0; + return height_; } @@ -341,69 +1250,303 @@ void BufferView::setCursor(DocIterator const & dit) { size_t const n = dit.depth(); for (size_t i = 0; i < n; ++i) - dit[i].inset().edit(cursor(), true); + dit[i].inset().edit(cursor_, true); - cursor().setCursor(dit); - cursor().selection() = false; + cursor_.setCursor(dit); + cursor_.selection() = false; +} + + +bool BufferView::checkDepm(LCursor & cur, LCursor & old) +{ + // Would be wrong to delete anything if we have a selection. + if (cur.selection()) + return false; + + bool need_anchor_change = false; + bool changed = cursor_.text()->deleteEmptyParagraphMechanism(cur, old, + need_anchor_change); + + if (need_anchor_change) + cur.resetAnchor(); + + if (!changed) + return false; + + updateMetrics(false); + buffer_->changed(); + return true; } -void BufferView::mouseSetCursor(LCursor & cur) +bool BufferView::mouseSetCursor(LCursor & cur) { BOOST_ASSERT(&cur.bv() == this); // Has the cursor just left the inset? - if (&cursor().inset() != &cur.inset()) - cursor().inset().notifyCursorLeaves(cursor()); + bool badcursor = false; + if (&cursor_.inset() != &cur.inset()) + badcursor = cursor_.inset().notifyCursorLeaves(cursor_); // do the dEPM magic if needed - if (cursor().inTexted()) - cursor().text()->deleteEmptyParagraphMechanism(cur, cursor()); + // FIXME: move this to InsetText::notifyCursorLeaves? + bool update = false; + if (!badcursor && cursor_.inTexted()) + checkDepm(cur, cursor_); + + // if the cursor was in an empty script inset and the new + // position is in the nucleus of the inset, notifyCursorLeaves + // will kill the script inset itself. So we check all the + // elements of the cursor to make sure that they are correct. + // For an example, see bug 2933: + // http://bugzilla.lyx.org/show_bug.cgi?id=2933 + // The code below could maybe be moved to a DocIterator method. + //lyxerr << "cur before " << cur <cursor_; + return cursor_; } LCursor const & BufferView::cursor() const { - return pimpl_->cursor_; + return cursor_; } -lyx::pit_type BufferView::anchor_ref() const +pit_type BufferView::anchor_ref() const { - return pimpl_->anchor_ref_; + return anchor_ref_; } -int BufferView::offset_ref() const +ViewMetricsInfo const & BufferView::viewMetricsInfo() { - return pimpl_->offset_ref_; + return metrics_info_; } + + +// FIXME: We should split-up updateMetrics() for the singlepar case. +void BufferView::updateMetrics(bool singlepar) +{ + LyXText & buftext = buffer_->text(); + TextMetrics & tm = textMetrics(&buftext); + pit_type size = int(buftext.paragraphs().size()); + + if (anchor_ref_ > int(buftext.paragraphs().size() - 1)) { + anchor_ref_ = int(buftext.paragraphs().size() - 1); + offset_ref_ = 0; + } + + // If the paragraph metrics has changed, we can not + // use the singlepar optimisation. + if (singlepar + // In Single Paragraph mode, rebreak only + // the (main text, not inset!) paragraph containing the cursor. + // (if this paragraph contains insets etc., rebreaking will + // recursively descend) + && tm.redoParagraph(cursor_.bottom().pit())) + singlepar = false; + + pit_type const pit = anchor_ref_; + int pit1 = pit; + int pit2 = pit; + size_t const npit = buftext.paragraphs().size(); + + // Rebreak anchor paragraph. + if (!singlepar) + tm.redoParagraph(pit); + + // Clear out the position cache in case of full screen redraw. + if (!singlepar) + coord_cache_.clear(); + + int y0 = tm.parMetrics(pit).ascent() - offset_ref_; + + // Redo paragraphs above anchor if necessary. + int y1 = y0; + while (y1 > 0 && pit1 > 0) { + y1 -= tm.parMetrics(pit1).ascent(); + --pit1; + if (!singlepar) + tm.redoParagraph(pit1); + y1 -= tm.parMetrics(pit1).descent(); + } + + + // Take care of ascent of first line + y1 -= tm.parMetrics(pit1).ascent(); + + // Normalize anchor for next time + anchor_ref_ = pit1; + offset_ref_ = -y1; + + // Grey at the beginning is ugly + if (pit1 == 0 && y1 > 0) { + y0 -= y1; + y1 = 0; + anchor_ref_ = 0; + } + + // Redo paragraphs below the anchor if necessary. + int y2 = y0; + while (y2 < height_ && pit2 < int(npit) - 1) { + y2 += tm.parMetrics(pit2).descent(); + ++pit2; + if (!singlepar) + tm.redoParagraph(pit2); + y2 += tm.parMetrics(pit2).ascent(); + } + + // Take care of descent of last line + y2 += tm.parMetrics(pit2).descent(); + + // The coordinates of all these paragraphs are correct, cache them + int y = y1; + CoordCache::InnerParPosCache & parPos = coord_cache_.parPos()[&buftext]; + for (pit_type pit = pit1; pit <= pit2; ++pit) { + ParagraphMetrics const & pm = tm.parMetrics(pit); + y += pm.ascent(); + parPos[pit] = Point(0, y); + if (singlepar && pit == cursor_.bottom().pit()) { + // In Single Paragraph mode, collect here the + // y1 and y2 of the (one) paragraph the cursor is in + y1 = y - pm.ascent(); + y2 = y + pm.descent(); + } + y += pm.descent(); + } + + if (singlepar) { + // collect cursor paragraph iter bounds + pit1 = cursor_.bottom().pit(); + pit2 = cursor_.bottom().pit(); + } + + lyxerr[Debug::DEBUG] + << BOOST_CURRENT_FUNCTION + << " y1: " << y1 + << " y2: " << y2 + << " pit1: " << pit1 + << " pit2: " << pit2 + << " npit: " << npit + << " singlepar: " << singlepar + << "size: " << size + << endl; + + metrics_info_ = ViewMetricsInfo(pit1, pit2, y1, y2, + singlepar? SingleParUpdate: FullScreenUpdate, size); + + if (lyxerr.debugging(Debug::WORKAREA)) { + lyxerr[Debug::WORKAREA] << "BufferView::updateMetrics" << endl; + coord_cache_.dump(); + } +} + + +void BufferView::menuInsertLyXFile(string const & filenm) +{ + BOOST_ASSERT(cursor_.inTexted()); + string filename = filenm; + + if (filename.empty()) { + // Launch a file browser + // FIXME UNICODE + string initpath = lyxrc.document_path; + + if (buffer_) { + string const trypath = buffer_->filePath(); + // If directory is writeable, use this as default. + if (isDirWriteable(FileName(trypath))) + initpath = trypath; + } + + // FIXME UNICODE + FileDialog fileDlg(_("Select LyX document to insert"), + LFUN_FILE_INSERT, + make_pair(_("Documents|#o#O"), from_utf8(lyxrc.document_path)), + make_pair(_("Examples|#E#e"), from_utf8(addPath(package().system_support(), "examples")))); + + FileDialog::Result result = + fileDlg.open(from_utf8(initpath), + FileFilterList(_("LyX Documents (*.lyx)")), + docstring()); + + if (result.first == FileDialog::Later) + return; + + // FIXME UNICODE + filename = to_utf8(result.second); + + // check selected filename + if (filename.empty()) { + // emit message signal. + message(_("Canceled.")); + return; + } + } + + // Get absolute path of file and add ".lyx" + // to the filename if necessary + filename = fileSearch(string(), filename, "lyx").absFilename(); + + docstring const disp_fn = makeDisplayPath(filename); + // emit message signal. + message(bformat(_("Inserting document %1$s..."), disp_fn)); + + docstring res; + Buffer buf("", false); + if (lyx::loadLyXFile(&buf, FileName(filename))) { + ErrorList & el = buffer_->errorList("Parse"); + // Copy the inserted document error list into the current buffer one. + el = buf.errorList("Parse"); + recordUndo(cursor_); + cap::pasteParagraphList(cursor_, buf.paragraphs(), + buf.params().textclass, el); + res = _("Document %1$s inserted."); + } else + res = _("Could not insert document %1$s"); + + // emit message signal. + message(bformat(res, disp_fn)); + buffer_->errors("Parse"); + resize(); +} + +} // namespace lyx