]> git.lyx.org Git - lyx.git/blobdiff - src/BufferView.cpp
Fix cursor when undoing accept/reject all changes
[lyx.git] / src / BufferView.cpp
index ad534eae2842500319bc7b796d24c59157307ea5..70d81567c1d3456b26e845e20b4fb67131fee71e 100644 (file)
@@ -31,7 +31,6 @@
 #include "Intl.h"
 #include "Language.h"
 #include "LayoutFile.h"
-#include "Lexer.h"
 #include "LyX.h"
 #include "LyXAction.h"
 #include "lyxfind.h"
@@ -59,6 +58,7 @@
 #include "mathed/MathRow.h"
 
 #include "frontends/alert.h"
+#include "frontends/Application.h"
 #include "frontends/CaretGeometry.h"
 #include "frontends/Delegates.h"
 #include "frontends/FontMetrics.h"
@@ -75,6 +75,7 @@
 #include "support/gettext.h"
 #include "support/lassert.h"
 #include "support/Length.h"
+#include "support/Lexer.h"
 #include "support/lstrings.h"
 #include "support/lyxlib.h"
 #include "support/types.h"
@@ -214,7 +215,7 @@ struct BufferView::Private
        Private(BufferView & bv) :
                update_strategy_(FullScreenUpdate),
                update_flags_(Update::Force),
-               cursor_(bv), anchor_pit_(0), anchor_ypos_(0),
+               cursor_(bv), anchor_pit_(0), anchor_ypos_(10000),
                wh_(0), inlineCompletionUniqueChars_(0),
                last_inset_(nullptr), mouse_position_cache_(),
                gui_(nullptr), bookmark_edit_position_(-1),
@@ -298,6 +299,13 @@ struct BufferView::Private
        frontend::CaretGeometry caret_geometry_;
        ///
        bool mouse_selecting_ = false;
+       /// Reference value for statistics (essentially subtract this from the actual value to see relative counts)
+       /// (words/chars/chars no blanks)
+       int stats_ref_value_w_ = 0;
+       int stats_ref_value_c_ = 0;
+       int stats_ref_value_nb_ = 0;
+       bool stats_update_trigger_ = false;
+
 };
 
 
@@ -503,7 +511,7 @@ bool BufferView::needsFitCursor() const
                int const asc = fm.maxAscent();
                int const des = fm.maxDescent();
                Point const p = getPos(d->cursor_);
-               if (p.y_ - asc >= 0 && p.y_ + des < height_)
+               if (p.y - asc >= 0 && p.y + des < height_)
                        return false;
        }
        return true;
@@ -532,7 +540,7 @@ void BufferView::processUpdateFlags(Update::flags flags)
                   << flagsAsString(flags) << ")  buffer: " << &buffer_);
 
        // Case when no explicit update is requested.
-       if (flags == Update::None)
+       if (flags == Update::None || !ready())
                return;
 
        /* FIXME We would like to avoid doing this here, since it is very
@@ -545,19 +553,26 @@ void BufferView::processUpdateFlags(Update::flags flags)
 
        // First check whether the metrics and inset positions should be updated
        if (flags & Update::Force) {
-               // This will update the CoordCache items and replace Force
-               // with ForceDraw in flags.
-               updateMetrics(flags);
+               // This will compute all metrics and positions.
+               updateMetrics(true);
+               // metrics is done, full drawing is necessary now
+               flags = (flags & ~Update::Force) | Update::ForceDraw;
+       }
+       /* If a single paragraph update has been requested and we are not
+        * already repainting all, check whether this update changes the
+        * paragraph metrics. If it does, then compute all metrics (in
+        * case the paragraph is in an inset)
+        *
+        * We handle this before FitCursor because the later will require
+        * correct metrics at cursor position.
+        */
+       else if ((flags & Update::SinglePar) && !(flags & Update::ForceDraw)) {
+               if (!singleParUpdate())
+                       updateMetrics(true);
        }
-
-       // Detect whether we can only repaint a single paragraph (if we
-       // are not already redrawing all).
-       // We handle this before FitCursor because the later will require
-       // correct metrics at cursor position.
-       if (!(flags & Update::ForceDraw)
-                       && (flags & Update::SinglePar)
-                       && !singleParUpdate())
-               updateMetrics(flags);
+       else if (flags & Update::ForceDraw)
+               // This will compute only the needed metrics and update positions.
+               updateMetrics(false);
 
        // Then make sure that the screen contains the cursor if needed
        if (flags & Update::FitCursor) {
@@ -565,19 +580,22 @@ void BufferView::processUpdateFlags(Update::flags flags)
                        // First try to make the selection start visible
                        // (which is just the cursor when there is no selection)
                        scrollToCursor(d->cursor_.selectionBegin(), SCROLL_VISIBLE);
-                       // Metrics have to be recomputed (maybe again)
-                       updateMetrics();
+                       // Metrics have to be updated
+                       updateMetrics(false);
                        // Is the cursor visible? (only useful if cursor is at end of selection)
                        if (needsFitCursor()) {
                                // then try to make cursor visible instead
                                scrollToCursor(d->cursor_, SCROLL_VISIBLE);
                                // Metrics have to be recomputed (maybe again)
-                               updateMetrics(flags);
+                               updateMetrics(false);
                        }
                }
-               flags = flags & ~Update::FitCursor;
+               flags = (flags & ~Update::FitCursor) | Update::ForceDraw;
        }
 
+       if (theApp()->drawStrategy() == DrawStrategy::Full)
+               flags = flags | Update::ForceDraw;
+
        // Add flags to the the update flags. These will be reset to None
        // after the redraw is actually done
        d->update_flags_ = d->update_flags_ | flags;
@@ -608,7 +626,7 @@ void BufferView::processUpdateFlags(Update::flags flags)
 
 void BufferView::updateScrollbarParameters()
 {
-       if (height_ == 0 && width_ == 0)
+       if (!ready())
                return;
 
        // We prefer fixed size line scrolling.
@@ -641,8 +659,8 @@ void BufferView::updateScrollbarParameters()
                        << d->par_height_[pit]);
        }
 
-       int top_pos = first.second->position() - first.second->ascent();
-       int bottom_pos = last.second->position() + last.second->descent();
+       int top_pos = first.second->top();
+       int bottom_pos = last.second->bottom();
        bool first_visible = first.first == 0 && top_pos >= 0;
        bool last_visible = last.first + 1 == int(parsize) && bottom_pos <= height_;
        if (first_visible && last_visible) {
@@ -754,10 +772,14 @@ void BufferView::scrollDocView(int const pixels, bool update)
        if (pixels == 0)
                return;
 
-       // If the offset is less than 2 screen height, prefer to scroll instead.
-       if (abs(pixels) <= 2 * height_) {
+       // If part of the existing paragraphs will remain visible, prefer to
+       // scroll
+       TextMetrics const & tm = textMetrics(&buffer_.text());
+       if (tm.first().second->top() - pixels <= height_
+            &&  tm.last().second->bottom() - pixels >= 0) {
+               LYXERR(Debug::SCROLLING, "small skip");
                d->anchor_ypos_ -= pixels;
-               processUpdateFlags(Update::Force);
+               processUpdateFlags(Update::ForceDraw);
                return;
        }
 
@@ -778,6 +800,7 @@ void BufferView::scrollDocView(int const pixels, bool update)
                return;
        }
 
+       LYXERR(Debug::SCROLLING, "search paragraph");
        // find paragraph at target position
        int par_pos = d->scrollbarParameters_.min;
        pit_type i = 0;
@@ -821,7 +844,7 @@ void BufferView::setCursorFromScrollbar()
                newy = last;
                break;
        case CUR_INSIDE:
-               int const y = getPos(oldcur).y_;
+               int const y = getPos(oldcur).y;
                newy = min(last, max(y, first));
                if (y == newy)
                        return;
@@ -862,9 +885,9 @@ Change const BufferView::getCurrentChange() const
 CursorStatus BufferView::cursorStatus(DocIterator const & dit) const
 {
        Point const p = getPos(dit);
-       if (p.y_ < 0)
+       if (p.y < 0)
                return CUR_ABOVE;
-       if (p.y_ > workHeight())
+       if (p.y > workHeight())
                return CUR_BELOW;
        return CUR_INSIDE;
 }
@@ -998,7 +1021,7 @@ void BufferView::showCursor(DocIterator const & dit, ScrollType how,
        bool update)
 {
        if (scrollToCursor(dit, how) && update)
-               processUpdateFlags(Update::Force);
+               processUpdateFlags(Update::ForceDraw);
 }
 
 
@@ -1037,7 +1060,7 @@ bool BufferView::scrollToCursor(DocIterator const & dit, ScrollType how)
                LBUFERR(!pm.rows().empty());
                // FIXME: smooth scrolling doesn't work in mathed.
                CursorSlice const & cs = dit.innerTextSlice();
-               int const ypos = pm.position() + coordOffset(dit).y_;
+               int const ypos = pm.position() + coordOffset(dit).y;
                ParagraphMetrics const & inner_pm =
                        textMetrics(cs.text()).parMetrics(cs.pit());
                Dimension const & row_dim =
@@ -1077,7 +1100,7 @@ bool BufferView::scrollToCursor(DocIterator const & dit, ScrollType how)
                d->inlineCompletionPos_ = DocIterator();
 
        tm.redoParagraph(bot_pit);
-       int const offset = coordOffset(dit).y_;
+       int const offset = coordOffset(dit).y;
        pit_type const old_pit = d->anchor_pit_;
        d->anchor_pit_ = bot_pit;
 
@@ -1091,8 +1114,6 @@ bool BufferView::scrollToCursor(DocIterator const & dit, ScrollType how)
        d->anchor_ypos_ = - offset + row_dim.ascent();
        if (how == SCROLL_CENTER)
                d->anchor_ypos_ += height_/2 - row_dim.height() / 2;
-       else if (!lyxrc.scroll_below_document && d->anchor_pit_ == max_pit)
-               d->anchor_ypos_ = height_ - offset - row_dim.descent();
        else if (offset > height_)
                d->anchor_ypos_ = height_ - offset - defaultRowHeight();
        else
@@ -1337,6 +1358,17 @@ bool BufferView::getStatus(FuncRequest const & cmd, FuncStatus & flag)
                flag.setEnabled(cur.selection());
                break;
 
+       case LFUN_STATISTICS_REFERENCE_CLAMP: {
+               // disable optitem reset if clamp not used
+               if  (cmd.argument() == "reset" && d->stats_ref_value_c_ == 0) {
+                               flag.setEnabled(false);
+                               break;
+               }
+               flag.setEnabled(true);
+               break;
+
+       }
+
        default:
                return false;
        }
@@ -1654,6 +1686,7 @@ void BufferView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                break;
 
        case LFUN_ALL_CHANGES_ACCEPT: {
+               UndoGroupHelper helper(cur);
                // select complete document
                cur.reset();
                cur.selHandle(true);
@@ -1673,6 +1706,7 @@ void BufferView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
        }
 
        case LFUN_ALL_CHANGES_REJECT: {
+               UndoGroupHelper helper(cur);
                // select complete document
                cur.reset();
                cur.selHandle(true);
@@ -2008,12 +2042,31 @@ void BufferView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
        }
                break;
 
+       case LFUN_STATISTICS_REFERENCE_CLAMP: {
+               d->stats_update_trigger_ = true;
+               if  (cmd.argument() == "reset") {
+                       d->stats_ref_value_w_ = d->stats_ref_value_c_ = d->stats_ref_value_nb_ = 0;
+                       break;
+               }
+
+               DocIterator from, to;
+               from = doc_iterator_begin(&buffer_);
+               to = doc_iterator_end(&buffer_);
+               buffer_.updateStatistics(from, to);
+
+               d->stats_ref_value_w_ = buffer_.wordCount();
+               d->stats_ref_value_c_ = buffer_.charCount(true);
+               d->stats_ref_value_nb_ = buffer_.charCount(false);
+               break;
+       }
+
+
        case LFUN_SCREEN_UP:
        case LFUN_SCREEN_DOWN: {
                Point p = getPos(cur);
                // This code has been commented out to enable to scroll down a
                // document, even if there are large insets in it (see bug #5465).
-               /*if (p.y_ < 0 || p.y_ > height_) {
+               /*if (p.y < 0 || p.y > height_) {
                        // The cursor is off-screen so recenter before proceeding.
                        showCursor();
                        p = getPos(cur);
@@ -2031,7 +2084,7 @@ void BufferView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                if (scrolled)
                        processUpdateFlags(Update::Force);
 
-               d->text_metrics_[&buffer_.text()].editXY(cur, p.x_, p.y_,
+               d->text_metrics_[&buffer_.text()].editXY(cur, p.x, p.y,
                        true, act == LFUN_SCREEN_UP);
                //FIXME: what to do with cur.x_target()?
                bool update = in_texted && cur.bv().checkDepm(cur, old);
@@ -2076,10 +2129,10 @@ void BufferView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                        cur.finishUndo();
                        break;
                }
-               int y = getPos(cur).y_;
+               int y = getPos(cur).y;
                int const ymin = y - height_ + defaultRowHeight();
                while (y > ymin && cur.up())
-                       y = getPos(cur).y_;
+                       y = getPos(cur).y;
 
                cur.finishUndo();
                dr.screenUpdate(Update::SinglePar | Update::FitCursor);
@@ -2094,10 +2147,10 @@ void BufferView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                        cur.finishUndo();
                        break;
                }
-               int y = getPos(cur).y_;
+               int y = getPos(cur).y;
                int const ymax = y + height_ - defaultRowHeight();
                while (y < ymax && cur.down())
-                       y = getPos(cur).y_;
+                       y = getPos(cur).y;
 
                cur.finishUndo();
                dr.screenUpdate(Update::SinglePar | Update::FitCursor);
@@ -2478,14 +2531,19 @@ void BufferView::clearSelection()
 
 void BufferView::resize(int width, int height)
 {
-       // Update from work area
-       width_ = width;
        height_ = height;
+       // Update metrics only if width has changed
+       if (width != width_) {
+               width_ = width;
 
-       // Clear the paragraph height cache.
-       d->par_height_.clear();
-       // Redo the metrics.
-       updateMetrics();
+               // Clear the paragraph height cache.
+               d->par_height_.clear();
+               // Redo the metrics.
+               updateMetrics(true);
+       }
+       // metrics is OK, full drawing is necessary now
+       d->update_flags_ = (d->update_flags_ & ~Update::Force) | Update::ForceDraw;
+       d->update_strategy_ = FullScreenUpdate;
 }
 
 
@@ -2553,8 +2611,8 @@ Inset const * BufferView::clickableMathInset(InsetMathNest const * inset,
 void BufferView::updateHoveredInset() const
 {
        // Get inset under mouse, if there is one.
-       int const x = d->mouse_position_cache_.x_;
-       int const y = d->mouse_position_cache_.y_;
+       int const x = d->mouse_position_cache_.x;
+       int const y = d->mouse_position_cache_.y;
        Inset const * covering_inset = getCoveringInset(buffer_.text(), x, y);
        if (covering_inset && covering_inset->asInsetMath()) {
                Inset const * inner_inset = clickableMathInset(
@@ -2587,8 +2645,8 @@ void BufferView::updateHoveredInset() const
 
        if (need_redraw) {
                LYXERR(Debug::PAINTING, "Mouse hover detected at: ("
-                               << d->mouse_position_cache_.x_ << ", "
-                               << d->mouse_position_cache_.y_ << ")");
+                               << d->mouse_position_cache_.x << ", "
+                               << d->mouse_position_cache_.y << ")");
 
                d->update_strategy_ = DecorationUpdate;
 
@@ -2615,27 +2673,43 @@ bool BufferView::mouseSelecting() const
 }
 
 
+int BufferView::stats_ref_value_w() const
+{
+       return d->stats_ref_value_w_;
+}
+
+
+int BufferView::stats_ref_value_c() const
+{
+       return d->stats_ref_value_c_;
+}
+
+
+int BufferView::stats_ref_value_nb() const
+{
+       return d->stats_ref_value_nb_;
+}
+
+
 void BufferView::mouseEventDispatch(FuncRequest const & cmd0)
 {
        //lyxerr << "[ cmd0 " << cmd0 << "]" << endl;
 
+       if (!ready())
+               return;
+
        // This is only called for mouse related events including
        // LFUN_FILE_OPEN generated by drag-and-drop.
        FuncRequest cmd = cmd0;
 
-       Cursor old = cursor();
-       Cursor cur(*this);
-       cur.push(buffer_.inset());
-       cur.selection(d->cursor_.selection());
-
        // Either the inset under the cursor or the
        // surrounding Text will handle this event.
 
        // make sure we stay within the screen...
        cmd.set_y(min(max(cmd.y(), -1), height_));
 
-       d->mouse_position_cache_.x_ = cmd.x();
-       d->mouse_position_cache_.y_ = cmd.y();
+       d->mouse_position_cache_.x = cmd.x();
+       d->mouse_position_cache_.y = cmd.y();
 
        d->mouse_selecting_ =
                cmd.action() == LFUN_MOUSE_MOTION && cmd.button() == mouse_button::button1;
@@ -2645,6 +2719,11 @@ void BufferView::mouseEventDispatch(FuncRequest const & cmd0)
                return;
        }
 
+       Cursor old = cursor();
+       Cursor cur(*this);
+       cur.push(buffer_.inset());
+       cur.selection(d->cursor_.selection());
+
        // Build temporary cursor.
        Inset * inset = d->text_metrics_[&buffer_.text()].editXY(cur, cmd.x(), cmd.y());
        if (inset) {
@@ -2726,7 +2805,7 @@ int BufferView::scrollDown(int pixels)
        int const ymax = height_ + pixels;
        while (true) {
                pair<pit_type, ParagraphMetrics const *> last = tm.last();
-               int bottom_pos = last.second->position() + last.second->descent();
+               int bottom_pos = last.second->bottom();
                if (lyxrc.scroll_below_document)
                        bottom_pos += height_ - minVisiblePart();
                if (last.first + 1 == int(text->paragraphs().size())) {
@@ -2751,7 +2830,7 @@ int BufferView::scrollUp(int pixels)
        int ymin = - pixels;
        while (true) {
                pair<pit_type, ParagraphMetrics const *> first = tm.first();
-               int top_pos = first.second->position() - first.second->ascent();
+               int top_pos = first.second->top();
                if (first.first == 0) {
                        if (top_pos >= 0)
                                return 0;
@@ -2993,6 +3072,25 @@ void BufferView::putSelectionAt(DocIterator const & cur,
 }
 
 
+void BufferView::setSelection(DocIterator const & from,
+                             DocIterator const & to)
+{
+       if (from.pit() != to.pit()) {
+               // there are multiple paragraphs in selection
+               cursor().setCursor(from);
+               cursor().clearSelection();
+               cursor().selection(true);
+               cursor().setCursor(to);
+               cursor().selection(true);
+       } else {
+               // only single paragraph
+               int const size = to.pos() - from.pos();
+               putSelectionAt(from, size, false);
+       }
+       processUpdateFlags(Update::Force | Update::FitCursor);
+}
+
+
 bool BufferView::selectIfEmpty(DocIterator & cur)
 {
        if ((cur.inTexted() && !cur.paragraph().empty())
@@ -3044,24 +3142,36 @@ Cursor const & BufferView::cursor() const
 
 bool BufferView::singleParUpdate()
 {
-       Text & buftext = buffer_.text();
-       pit_type const bottom_pit = d->cursor_.bottom().pit();
-       TextMetrics & tm = textMetrics(&buftext);
-       Dimension const old_dim = tm.parMetrics(bottom_pit).dim();
+       CursorSlice const & its = d->cursor_.innerTextSlice();
+       pit_type const pit = its.pit();
+       TextMetrics & tm = textMetrics(its.text());
+       Dimension const old_dim = tm.parMetrics(pit).dim();
 
        // make sure inline completion pointer is ok
        if (d->inlineCompletionPos_.fixIfBroken())
                d->inlineCompletionPos_ = DocIterator();
 
-       // 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(bottom_pit);
-       ParagraphMetrics & pm = tm.parMetrics(bottom_pit);
-       if (pm.height() != old_dim.height()) {
-               // Paragraph height has changed so we cannot proceed to
-               // the singlePar optimisation.
+       /* Try to rebreak only the paragraph containing the cursor (if
+        * this paragraph contains insets etc., rebreaking will
+        * recursively descend). We need a full redraw if either
+        * 1/ the height has changed
+        * or
+        * 2/ the width has changed and it was equal to the textmetrics
+        *    width; the goal is to catch the case of a one-row inset that
+        *    grows with its contents, but optimize the case of typing at
+        *    the end of a mmultiple-row paragraph.
+        *
+        * NOTE: if only the height has changed, then it should be
+        *   possible to update all metrics at minimal cost. However,
+        *   since this is risky, we do not try that right now.
+        */
+       tm.redoParagraph(pit);
+       ParagraphMetrics & pm = tm.parMetrics(pit);
+       if (pm.height() != old_dim.height()
+               || (pm.width() != old_dim.width() && old_dim.width() == tm.width())) {
+               // Paragraph height or width has changed so we cannot proceed
+               // to the singlePar optimisation.
+               LYXERR(Debug::PAINTING, "SinglePar optimization failed.");
                return false;
        }
        // Since position() points to the baseline of the first row, we
@@ -3069,111 +3179,102 @@ bool BufferView::singleParUpdate()
        // the height does not change but the ascent does.
        pm.setPosition(pm.position() - old_dim.ascent() + pm.ascent());
 
-       tm.updatePosCache(bottom_pit);
+       tm.updatePosCache(pit);
 
-       LYXERR(Debug::PAINTING, "\ny1: " << pm.position() - pm.ascent()
-               << " y2: " << pm.position() + pm.descent()
-               << " pit: " << bottom_pit
-               << " singlepar: 1");
+       LYXERR(Debug::PAINTING, "\ny1: " << pm.top() << " y2: " << pm.bottom()
+               << " pit: " << pit << " singlepar: 1");
        return true;
 }
 
 
 void BufferView::updateMetrics()
 {
-       updateMetrics(d->update_flags_);
+       updateMetrics(true);
+       // metrics is done, full drawing is necessary now
+       d->update_flags_ = (d->update_flags_ & ~Update::Force) | Update::ForceDraw;
        d->update_strategy_ = FullScreenUpdate;
 }
 
 
-void BufferView::updateMetrics(Update::flags & update_flags)
+void BufferView::updateMetrics(bool force)
 {
-       if (height_ == 0 || width_ == 0)
+       if (!ready())
                return;
 
+       //LYXERR0("updateMetrics " << _v_(force));
+
        Text & buftext = buffer_.text();
-       pit_type const npit = int(buftext.paragraphs().size());
+       pit_type const lastpit = int(buftext.paragraphs().size()) - 1;
 
-       // Clear out the position cache in case of full screen redraw,
-       d->coord_cache_.clear();
-       d->math_rows_.clear();
+       if (force) {
+               // Clear out the position cache in case of full screen redraw,
+               d->coord_cache_.clear();
+               d->math_rows_.clear();
 
-       // Clear out paragraph metrics to avoid having invalid metrics
-       // in the cache from paragraphs not relayouted below
-       // The complete text metrics will be redone.
-       d->text_metrics_.clear();
+               // Clear out paragraph metrics to avoid having invalid metrics
+               // in the cache from paragraphs not relayouted below. The
+               // complete text metrics will be redone.
+               d->text_metrics_.clear();
+       }
 
+       // This should not be moved earlier
        TextMetrics & tm = textMetrics(&buftext);
 
        // make sure inline completion pointer is ok
        if (d->inlineCompletionPos_.fixIfBroken())
                d->inlineCompletionPos_ = DocIterator();
 
-       if (d->anchor_pit_ >= npit)
+       if (d->anchor_pit_ > lastpit)
                // The anchor pit must have been deleted...
-               d->anchor_pit_ = npit - 1;
+               d->anchor_pit_ = lastpit;
 
-       // Rebreak anchor paragraph.
-       tm.redoParagraph(d->anchor_pit_);
-       ParagraphMetrics & anchor_pm = tm.parMetrics(d->anchor_pit_);
+       // Update metrics around the anchor
+       tm.updateMetrics(d->anchor_pit_, d->anchor_ypos_, height_);
 
-       // position anchor
-       if (d->anchor_pit_ == 0) {
-               int scrollRange = d->scrollbarParameters_.max - d->scrollbarParameters_.min;
+       // Check that the end of the document is not too high
+       int const min_visible = lyxrc.scroll_below_document ? minVisiblePart() : height_;
+       if (tm.last().first == lastpit && tm.last().second->hasPosition()
+            && tm.last().second->bottom() < min_visible) {
+               d->anchor_ypos_ += min_visible - tm.last().second->bottom();
+               LYXERR(Debug::SCROLLING, "Too high, adjusting anchor ypos to " << d->anchor_ypos_);
+               tm.updateMetrics(d->anchor_pit_, d->anchor_ypos_, height_);
+       }
 
-               // Complete buffer visible? Then it's easy.
-               if (scrollRange == 0)
-                       d->anchor_ypos_ = anchor_pm.ascent();
-               else {
-                       // avoid empty space above the first row
-                       d->anchor_ypos_ = min(d->anchor_ypos_, anchor_pm.ascent());
-               }
-       }
-       anchor_pm.setPosition(d->anchor_ypos_);
-       tm.updatePosCache(d->anchor_pit_);
-
-       LYXERR(Debug::PAINTING, "metrics: "
-               << " anchor pit = " << d->anchor_pit_
-               << " anchor ypos = " << d->anchor_ypos_);
-
-       // Redo paragraphs above anchor if necessary.
-       int y1 = d->anchor_ypos_ - anchor_pm.ascent();
-       // We are now just above the anchor paragraph.
-       pit_type pit1 = d->anchor_pit_ - 1;
-       for (; pit1 >= 0 && y1 >= 0; --pit1) {
-               tm.redoParagraph(pit1);
-               ParagraphMetrics & pm = tm.parMetrics(pit1);
-               y1 -= pm.descent();
-               // Save the paragraph position in the cache.
-               pm.setPosition(y1);
-               tm.updatePosCache(pit1);
-               y1 -= pm.ascent();
-       }
-
-       // Redo paragraphs below the anchor if necessary.
-       int y2 = d->anchor_ypos_ + anchor_pm.descent();
-       // We are now just below the anchor paragraph.
-       pit_type pit2 = d->anchor_pit_ + 1;
-       for (; pit2 < npit && y2 <= height_; ++pit2) {
-               tm.redoParagraph(pit2);
-               ParagraphMetrics & pm = tm.parMetrics(pit2);
-               y2 += pm.ascent();
-               // Save the paragraph position in the cache.
-               pm.setPosition(y2);
-               tm.updatePosCache(pit2);
-               y2 += pm.descent();
-       }
-
-       LYXERR(Debug::PAINTING, "Metrics: "
-               << " anchor pit = " << d->anchor_pit_
-               << " anchor ypos = " << d->anchor_ypos_
-               << " y1 = " << y1
-               << " y2 = " << y2
-               << " pit1 = " << pit1
-               << " pit2 = " << pit2);
+       // Check that the start of the document is not too low
+       if (tm.first().first == 0 && tm.first().second->hasPosition()
+            && tm.first().second->top() > 0) {
+               d->anchor_ypos_ -= tm.first().second->top();
+               LYXERR(Debug::SCROLLING, "Too low, adjusting anchor ypos to " << d->anchor_ypos_);
+               tm.updateMetrics(d->anchor_pit_, d->anchor_ypos_, height_);
+       }
 
-       // metrics is done, full drawing is necessary now
-       update_flags = (update_flags & ~Update::Force) | Update::ForceDraw;
+       /* FIXME: do we want that? It avoids potential issues with old
+        * paragraphs that should have been recomputed but have not, at
+        * the price of potential extra metrics computation. I do not
+        * think that the performance gain is high, so that for now the
+        * extra paragraphs are removed
+        */
+       // Remove paragraphs that are outside of screen
+       while(!tm.first().second->hasPosition() || tm.first().second->bottom() <= 0) {
+               //LYXERR0("Forget pit: " << tm.first().first);
+               tm.forget(tm.first().first);
+       }
+       while(!tm.last().second->hasPosition() || tm.last().second->top() > height_) {
+               //LYXERR0("Forget pit: " << tm.first().first);
+               tm.forget(tm.last().first);
+       }
+
+       /* FIXME: if paragraphs outside of the screen are not removed
+        * above, one has to search for the first visible one here */
+       // Normalize anchor for next time
+       if (d->anchor_pit_ != tm.first().first
+           || d->anchor_ypos_ != tm.first().second->position()) {
+               LYXERR(Debug::PAINTING, __func__ << ": Found new anchor pit = " << tm.first().first
+                               << "  anchor ypos = " << tm.first().second->position()
+                               << " (was " << d->anchor_pit_ << ", " << d->anchor_ypos_ << ")");
+               d->anchor_pit_ = tm.first().first;
+               d->anchor_ypos_ = tm.first().second->position();
+       }
 
        // Now update the positions of insets in the cache.
        updatePosCache();
@@ -3320,7 +3421,7 @@ Point BufferView::getPos(DocIterator const & dit) const
 
        // offset from outer paragraph
        Point p = coordOffset(dit);
-       p.y_ += tm.parMetrics(bot.pit()).position();
+       p.y += tm.parMetrics(bot.pit()).position();
        return p;
 }
 
@@ -3354,9 +3455,9 @@ void BufferView::caretPosAndDim(Point & p, Dimension & dim) const
 
        p = getPos(cur);
        // center fat carets horizontally
-       p.x_ -= dim.wid / 2;
+       p.x -= dim.wid / 2;
        // p is top-left
-       p.y_ -= dim.asc;
+       p.y -= dim.asc;
 }
 
 
@@ -3380,10 +3481,10 @@ void BufferView::buildCaretGeometry(bool complet)
        bool const slant = fm.italic() && cur.inTexted() && !cur.selection();
        double const slope = slant ? fm.italicSlope() : 0;
        cg.shapes.push_back(
-               {{iround(p.x_ + dim.asc * slope),                 p.y_},
-                {iround(p.x_ - dim.des * slope),                 p.y_ + dim.height()},
-                {iround(p.x_ + dir * dim.wid - dim.des * slope), p.y_ + dim.height()},
-                {iround(p.x_ + dir * dim.wid + dim.asc * slope), p.y_}}
+               {{iround(p.x + dim.asc * slope),                 p.y},
+                {iround(p.x - dim.des * slope),                 p.y + dim.height()},
+                {iround(p.x + dir * dim.wid - dim.des * slope), p.y + dim.height()},
+                {iround(p.x + dir * dim.wid + dim.asc * slope), p.y}}
                );
 
        // The language indicator _| (if needed)
@@ -3391,8 +3492,8 @@ void BufferView::buildCaretGeometry(bool complet)
        if (!((realfont.language() == doclang && isrtl == doclang->rightToLeft())
                  || realfont.language() == latex_language)) {
                int const lx = dim.height() / 3;
-               int const xx = iround(p.x_ - dim.des * slope);
-               int const yy = p.y_ + dim.height();
+               int const xx = iround(p.x - dim.des * slope);
+               int const yy = p.y + dim.height();
                cg.shapes.push_back(
                        {{xx,                            yy - dim.wid},
                         {xx + dir * (dim.wid + lx - 1), yy - dim.wid},
@@ -3403,12 +3504,12 @@ void BufferView::buildCaretGeometry(bool complet)
 
        // The completion triangle |> (if needed)
        if (complet) {
-               int const m = p.y_ + dim.height() / 2;
+               int const m = p.y + dim.height() / 2;
                int const d = dim.height() / 8;
                // offset for slanted carret
                int const sx = iround((dim.asc - (dim.height() / 2 - d)) * slope);
                // starting position x
-               int const xx = p.x_ + dir * dim.wid + sx;
+               int const xx = p.x + dir * dim.wid + sx;
                cg.shapes.push_back(
                        {{xx,                     m - d},
                         {xx + dir * d,           m},
@@ -3426,10 +3527,10 @@ void BufferView::buildCaretGeometry(bool complet)
        cg.bottom = -1000000;
        for (auto const & shape : cg.shapes)
                for (Point const & p : shape) {
-                       cg.left = min(cg.left, p.x_);
-                       cg.right = max(cg.right, p.x_);
-                       cg.top = min(cg.top, p.y_);
-                       cg.bottom = max(cg.bottom, p.y_);
+                       cg.left = min(cg.left, p.x);
+                       cg.right = max(cg.right, p.x);
+                       cg.top = min(cg.top, p.y);
+                       cg.bottom = max(cg.bottom, p.y);
                }
 }
 
@@ -3449,7 +3550,7 @@ bool BufferView::caretInView() const
        caretPosAndDim(p, dim);
 
        // does the cursor touch the screen ?
-       if (p.y_ + dim.height() < 0 || p.y_ >= workHeight())
+       if (p.y + dim.height() < 0 || p.y >= workHeight())
                return false;
        return true;
 }
@@ -3517,7 +3618,7 @@ void BufferView::checkCursorScrollOffset()
        setCurrentRowSlice(rowSlice);
 
        // Current x position of the cursor in pixels
-       int cur_x = getPos(d->cursor_).x_;
+       int cur_x = getPos(d->cursor_).x;
 
        // Horizontal scroll offset of the cursor row in pixels
        int offset = d->horiz_scroll_offset_;
@@ -3563,7 +3664,7 @@ bool BufferView::busy() const
 
 void BufferView::draw(frontend::Painter & pain, bool paint_caret)
 {
-       if (height_ == 0 || width_ == 0)
+       if (!ready())
                return;
        LYXERR(Debug::PAINTING, (pain.isNull() ? "\t\t--- START NODRAW ---"
                                 : "\t\t*** START DRAWING ***"));
@@ -3623,17 +3724,21 @@ void BufferView::draw(frontend::Painter & pain, bool paint_caret)
                // Draw everything.
                tm.draw(pi, 0, y);
 
-               // and possibly grey out below
+               break;
+       }
+
+       // Possibly grey out below
+       if (d->update_strategy_ != NoScreenUpdate) {
                pair<pit_type, ParagraphMetrics const *> lastpm = tm.last();
-               int const y2 = lastpm.second->position() + lastpm.second->descent();
+               int const y2 = lastpm.second->bottom();
 
                if (y2 < height_) {
                        Color color = buffer().isInternal()
                                ? Color_background : Color_bottomarea;
                        pain.fillRectangle(0, y2, width_, height_ - y2, color);
                }
-               break;
        }
+
        LYXERR(Debug::PAINTING, (pain.isNull() ? "\t\t --- END NODRAW ---"
                                : "\t\t *** END DRAWING ***"));
 
@@ -3641,21 +3746,25 @@ void BufferView::draw(frontend::Painter & pain, bool paint_caret)
        // FIXME: does it always? see ticket #11947.
        updateScrollbarParameters();
 
-       // Normalize anchor for next time
+       // Normalize anchor for next time (in case updateMetrics did not do it yet)
+       // FIXME: is this useful?
        pair<pit_type, ParagraphMetrics const *> firstpm = tm.first();
        pair<pit_type, ParagraphMetrics const *> lastpm = tm.last();
        for (pit_type pit = firstpm.first; pit <= lastpm.first; ++pit) {
                ParagraphMetrics const & pm = tm.parMetrics(pit);
-               if (pm.position() + pm.descent() > 0) {
+               if (pm.bottom() > 0) {
                        if (d->anchor_pit_ != pit
                            || d->anchor_ypos_ != pm.position())
-                               LYXERR(Debug::PAINTING, "Found new anchor pit = " << d->anchor_pit_
-                                      << "  anchor ypos = " << d->anchor_ypos_);
+                               LYXERR0(__func__ << ": Found new anchor pit = " << pit
+                                               << "  anchor ypos = " << pm.position()
+                                               << " (was " << d->anchor_pit_ << ", " << d->anchor_ypos_ << ")"
+                                                  "\nIf you see this message, please report.");
                        d->anchor_pit_ = pit;
                        d->anchor_ypos_ = pm.position();
                        break;
                }
        }
+
        if (!pain.isNull()) {
                // reset the update flags, everything has been done
                d->update_flags_ = Update::None;
@@ -3672,8 +3781,8 @@ void BufferView::draw(frontend::Painter & pain, bool paint_caret)
                        if (!cur.inTexted())
                                break;
                        TextMetrics const & tm = textMetrics(cur.text());
-                       if (d->caret_geometry_.left >= tm.origin().x_
-                               && d->caret_geometry_.right <= tm.origin().x_ + tm.dim().width())
+                       if (d->caret_geometry_.left >= tm.origin().x
+                               && d->caret_geometry_.right <= tm.origin().x + tm.dim().width())
                                break;
                        cur.pop();
                }
@@ -3849,4 +3958,14 @@ bool BufferView::clickableInset() const
        return d->clickable_inset_;
 }
 
+
+bool BufferView::stats_update_trigger()
+{
+       if (d->stats_update_trigger_) {
+               d->stats_update_trigger_ = false;
+               return true;
+       }
+       return false;
+}
+
 } // namespace lyx