org.eclipse.fx.ui.controls.styledtext.internal.ContentView.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.fx.ui.controls.styledtext.internal.ContentView.java

Source

/*******************************************************************************
 * Copyright (c) 2016 BestSolution.at and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation
 *******************************************************************************/
package org.eclipse.fx.ui.controls.styledtext.internal;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.eclipse.fx.ui.controls.Util;
import org.eclipse.fx.ui.controls.styledtext.StyledTextArea;
import org.eclipse.fx.ui.controls.styledtext.StyledTextArea.LineLocation;
import org.eclipse.fx.ui.controls.styledtext.StyledTextContent;
import org.eclipse.fx.ui.controls.styledtext.StyledTextContent.TextChangeListener;
import org.eclipse.fx.ui.controls.styledtext.TextChangedEvent;
import org.eclipse.fx.ui.controls.styledtext.TextChangingEvent;
import org.eclipse.fx.ui.controls.styledtext.TextSelection;
import org.eclipse.fx.ui.controls.styledtext.events.HoverTarget;
import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotationPresenter;

import com.google.common.collect.ContiguousSet;
import com.google.common.collect.DiscreteDomain;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;

import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.SetProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleSetProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;

@SuppressWarnings("javadoc")
public class ContentView extends Pane {

    private SetProperty<TextAnnotationPresenter> textAnnotationPresenter = new SimpleSetProperty<>(
            FXCollections.observableSet());

    public SetProperty<TextAnnotationPresenter> textAnnotationPresenterProperty() {
        return this.textAnnotationPresenter;
    }

    private class LineLayer extends VFlow<LineNode> {

        public LineLayer(Supplier<LineNode> nodeFactory, BiConsumer<LineNode, Integer> nodePopulator) {
            super(nodeFactory, nodePopulator);

            setOnRelease(n -> n.release());
            setOnActivate((idx, n) -> n.setIndex(idx.intValue()));
        }

        //      protected void releaseNode(int lineIndex) {
        //
        //         get(model.get(lineIndex)).ifPresent(n->{
        //            n.setVisible(false);
        //            n.release();
        //         });
        //      }

        //      protected void releaseNode(StyledTextLine line) {
        //         release(line);
        ////         get(line).ifPresent(n->{
        ////            n.setVisible(false);
        ////            n.release();
        ////         });
        //      }

        //      private void updateNode(StyledTextLine line) {
        //         LineNode node = get(line);
        //         node.update(line, textAnnotationPresenter);
        ////         LineNode node = getCreate(m);
        ////         node.setVisible(true);
        //////         node.setModel(m);
        ////         node.update(m, textAnnotationPresenter);
        //      }

        //      private void updateNode(int lineIndex, StyledTextLine m) {
        //         LineNode node = getCreate(m);
        //         node.setVisible(true);
        ////         node.setModel(m);
        //         node.update(m, textAnnotationPresenter);
        //      }

        //      @Override
        //      public void requestLayout() {
        //         super.requestLayout();
        //      }

        //      @Override
        //      protected void layoutChildren() {
        //         ContiguousSet.create(visibleLines.get(), DiscreteDomain.integers()).forEach(e -> {
        //            if (!yOffsetData.containsKey(e)) {
        //               return;
        //            }
        //            double x = 0;
        //            double y = yOffsetData.get(e);
        //            double width = getWidth();
        //            double height = getLineHeight();
        //
        //            if (model.size() > e) {
        //               StyledTextLine m = model.get(e);
        //               LineNode lineNode = get(m);
        //               lineNode.resizeRelocate(x, y, width, height);
        //
        ////               get(m).ifPresent(n->n.resizeRelocate(x, y, width, height));
        //
        //               lineNode.layout();
        //            }
        //         });
        //      }

        @Override
        protected void releaseNode(int lineIndex) {
            super.releaseNode(lineIndex);

        }

        private Stream<LineNode> createVisibleLineNodesStream() {
            ContiguousSet<Integer> visibleIndexes = ContiguousSet.create(ContentView.this.visibleLines.get(),
                    DiscreteDomain.integers());
            //         return visibleIndexes.stream().filter(i->i<model.size()).map(idx->get(model.get(idx))).filter(n->n.isPresent()).map(n->n.get()).filter(n->n.isVisible());
            return visibleIndexes.stream().filter(i -> i.intValue() < getNumberOfLines())
                    .map(idx -> getVisibleNode(idx.intValue())).filter(n -> n.isPresent()).map(n -> n.get());
        }

        Optional<Integer> getLineIndex(javafx.geometry.Point2D point) {
            Predicate<Node> filter = n -> {
                Bounds b = n.getBoundsInParent();
                return b.getMinY() <= point.getY() && point.getY() <= b.getMaxY();
            };
            final Optional<LineNode> hitLine = createVisibleLineNodesStream().filter(filter).findFirst();
            final Optional<Integer> index = hitLine.map(n -> {
                int i = n.getCaretIndexAtPoint(new javafx.geometry.Point2D(point.getX(), n.getHeight() / 2));
                if (i >= 0) {
                    return Integer.valueOf(n.getStartOffset() + i);
                } else if (point.getX() > 0) {
                    return Integer.valueOf(n.getEndOffset());
                } else if (point.getX() < 0) {
                    return Integer.valueOf(n.getStartOffset());
                } else {
                    return Integer.valueOf(-1);
                }
            });
            return index;
        }

        public List<HoverTarget> findHoverTargets(Point2D localLocation) {
            ContiguousSet<Integer> visibleIndexes = ContiguousSet.create(ContentView.this.visibleLines.get(),
                    DiscreteDomain.integers());
            return visibleIndexes.stream().map(lineIndex -> getVisibleNode(lineIndex)).filter(x -> x.isPresent())
                    .filter(x -> x.get().getBoundsInParent().contains(localLocation))
                    .flatMap(x -> x.get().findHoverTargets(x.get().parentToLocal(localLocation)).stream())
                    .collect(Collectors.toList());
        }

        public Optional<TextNode> findTextNode(Point2D localLocation) {
            ContiguousSet<Integer> visibleLineIndexes = ContiguousSet.create(ContentView.this.visibleLines.get(),
                    DiscreteDomain.integers());
            return visibleLineIndexes.stream().map(lineIndex -> getVisibleNode(lineIndex))
                    .filter(x -> x.isPresent()).filter(x -> x.get().getBoundsInParent().contains(localLocation))
                    .findFirst().flatMap(x -> x.get().findTextNode(x.get().parentToLocal(localLocation)));
        }

    }

    public List<HoverTarget> findHoverTargets(Point2D localLocation) {
        return this.lineLayer.findHoverTargets(localLocation);
    }

    public Optional<TextNode> findTextNode(Point2D localLocation) {
        localLocation = this.lineLayer.sceneToLocal(this.localToScene(localLocation));
        return this.lineLayer.findTextNode(localLocation);
    }

    private StackPane contentBody = new StackPane();

    private final LineLayer lineLayer;

    //   private Predicate<Set<LineNode>> needsPresentation;

    private Map<Integer, Double> yOffsetData = new HashMap<>();

    //   private boolean forceLayout = true;

    private DoubleProperty lineHeigth = new SimpleDoubleProperty(this, "lineHeight", 16.0); //$NON-NLS-1$

    public DoubleProperty lineHeightProperty() {
        return this.lineHeigth;
    }

    public double getLineHeight() {
        return this.lineHeigth.get();
    }

    public void setLineHeight(double lineHeight) {
        this.lineHeigth.set(lineHeight);
    }

    IntegerProperty numberOfLines = new SimpleIntegerProperty(this, "numberOfLines", 0); //$NON-NLS-1$

    public ReadOnlyIntegerProperty numberOfLinesProperty() {
        return this.numberOfLines;
    }

    public int getNumberOfLines() {
        return this.numberOfLines.get();
    }

    private IntegerProperty caretOffset = new SimpleIntegerProperty(this, "caretOffset", 0); //$NON-NLS-1$

    public IntegerProperty caretOffsetProperty() {
        return this.caretOffset;
    }

    private ObjectProperty<TextSelection> textSelection = new SimpleObjectProperty<>(this, "textSelection", //$NON-NLS-1$
            new TextSelection(0, 0));

    public ObjectProperty<TextSelection> textSelectionProperty() {
        return this.textSelection;
    }

    private ObjectProperty<StyledTextContent> content = new SimpleObjectProperty<>(this, "content"); //$NON-NLS-1$

    public ObjectProperty<StyledTextContent> contentProperty() {
        return this.content;
    }

    public StyledTextContent getContent() {
        return this.content.get();
    }

    private LineHelper lineHelper;

    protected LineLayer getLineLayer() {
        return this.lineLayer;
    }

    protected LineHelper getLineHelper() {
        return this.lineHelper;
    }

    //   private ObservableList<StyledTextLine> model;
    //   private IntegerBinding modelSize;

    //   public void setModel(ObservableList<StyledTextLine> model) {
    //      this.model = model;
    //
    //
    ////      model.addListener((InvalidationListener)(x)->prepareNodes(getVisibleLines()));
    //
    ////      model.addListener((ListChangeListener<StyledTextLine>)(c)->{
    ////         RangeSet<Integer> updateNodes = TreeRangeSet.create();
    ////
    ////         while (c.next()) {
    ////            if (c.wasPermutated()) {
    //////               for (int i = c.getFrom(); i < c.getTo(); i++) {
    //////                  lineLayer.permutate(i, c.getPermutation(i));
    //////               }
    //////               lineLayer.requestLayout();
    ////            }
    ////            if (c.wasUpdated() || c.wasReplaced()) {
    ////               updateNodes.add(Range.closedOpen(c.getFrom(), c.getTo()));
    ////
    ////
    ////            }
    ////            if (c.wasAdded()) {
    ////               updateNodes.add(Range.closedOpen(c.getFrom(), model.size()));
    ////            }
    ////            if (c.wasRemoved()) {
    ////
    ////               c.getRemoved().forEach(line->lineLayer.releaseNode(line));
    ////
    ////               updateNodes.add(Range.closedOpen(c.getFrom(), model.size()));
    ////            }
    ////         }
    ////
    ////         updateNodesNow(updateNodes);
    ////      });
    ////
    ////      modelSize = Bindings.size(model);
    ////
    ////      modelSize.addListener((x, o, n)-> {
    ////         int newSize = n.intValue();
    ////         int oldSize = o.intValue();
    ////         if (newSize < oldSize) {
    //////            for (int lineIdx = newSize; lineIdx < oldSize; lineIdx++) {
    //////               lineLayer.releaseNode(lineIdx);
    //////            }
    ////         }
    ////      });
    //   }

    //   private ListProperty<StyledTextLine> model = new SimpleListProperty<>(this, "model", FXCollections.observableArrayList());
    //   public ListProperty<StyledTextLine> getModel() {
    //      return this.model;
    //   }

    ObjectProperty<Range<Integer>> visibleLines = new SimpleObjectProperty<>(this, "visibleLines", //$NON-NLS-1$
            Range.closed(Integer.valueOf(0), Integer.valueOf(0)));

    public ObjectProperty<Range<Integer>> visibleLinesProperty() {
        return this.visibleLines;
    }

    public com.google.common.collect.Range<Integer> getVisibleLines() {
        return this.visibleLines.get();
    }

    public void setVisibleLines(Range<Integer> visibleLines) {
        this.visibleLines.set(visibleLines);
    }

    private Range<Integer> curVisibleLines;
    private StyledTextArea area;
    private boolean skipCSSApply;

    public ContentView(LineHelper lineHelper, StyledTextArea area) {
        this.lineLayer = new LineLayer(() -> new LineNode(area.tabAvanceProperty()), (n, m) -> {
            n.caretLayerVisibleProperty().bind(area.focusedProperty());
            n.setLineHelper(getLineHelper());
            n.updateInsertionMarkerIndex(this.insertionMarkerIndex);
            n.update(this.textAnnotationPresenter.get());
        });
        this.area = area;
        this.lineHelper = lineHelper;
        //      setStyle("-fx-border-color: green; -fx-border-width:2px; -fx-border-style: dashed;");
        this.contentBody.setPadding(new Insets(0, 0, 0, 2));
        this.contentBody.getChildren().setAll(this.lineLayer);

        //      this.lineLayer.setStyle("-fx-border-color: orange; -fx-border-width:2px; -fx-border-style: solid;");

        //      this.contentBody.setStyle("-fx-border-color: blue; -fx-border-width:2px; -fx-border-style: dotted;");

        this.getChildren().setAll(this.contentBody);

        //      AnimationTimer t = new AnimationTimer() {
        //         @Override
        //         public void handle(long now) {
        //            updatePulse(now);
        //         }
        //      };
        //
        //      visibleProperty().addListener((x, o, n)->{
        //         if (n) {
        //            t.start();
        //         }
        //         else {
        //            t.stop();
        //         }
        //      });
        //
        //      t.start();
        //      sceneProperty().addListener((x, o, n) -> {
        //         if (n == null) {
        //            t.stop();
        //         }
        //         else {
        //            t.start();
        //         }
        //      });

        setMinWidth(200);
        setMinHeight(200);

        this.visibleLines.addListener(this::onLineChange);
        this.offsetX.addListener(this::onLineChange);

        this.lineLayer.lineHeightProperty().bind(this.lineHeigth);
        this.lineLayer.yOffsetProperty().bind(this.offsetY);
        this.lineLayer.visibleLinesProperty().bind(this.visibleLines);
        this.lineLayer.numberOfLinesProperty().bind(this.numberOfLines);

        bindContentListener();
        bindCaretListener();
        bindSelectionListener();

        initBindings();
        this.charWidth.addListener(o -> this.cachedLongestLine = 0.0);
    }

    private DoubleBinding charWidth;

    private void initBindings() {
        DoubleBinding b0 = Util.createTextWidthBinding("M", this.area.fontProperty(), //$NON-NLS-1$
                this.area.fontZoomFactorProperty());
        this.charWidth = Bindings.createDoubleBinding(() -> Math.ceil(b0.get()), b0);
    }

    private void bindCaretListener() {
        this.caretOffset.addListener((x, o, n) -> {
            int oldCaretLine = getContent().getLineAtOffset(o.intValue());
            int newCaretLine = getContent().getLineAtOffset(n.intValue());

            RangeSet<Integer> toUpdate = TreeRangeSet.create();
            toUpdate.add(Range.closed(Integer.valueOf(oldCaretLine), Integer.valueOf(oldCaretLine)));
            toUpdate.add(Range.closed(Integer.valueOf(newCaretLine), Integer.valueOf(newCaretLine)));

            updateNodesNow(toUpdate);
        });
    }

    private Range<Integer> getAffectedLines(TextSelection selection) {
        int firstLine = getContent().getLineAtOffset(selection.offset);
        int lastLine = getContent().getLineAtOffset(selection.offset + selection.length);
        if (lastLine == -1) {
            lastLine = this.numberOfLines.get();
        }
        return Range.closed(Integer.valueOf(firstLine),
                Integer.valueOf(Math.min(lastLine, this.numberOfLines.get())));
    }

    //   private static Range<Integer> toRange(TextSelection s) {
    //      return Range.closedOpen(Integer.valueOf(s.offset), Integer.valueOf(s.offset + s.length));
    //   }
    //
    //   private Range<Integer> toLineRange(Range<Integer> globalOffsetRange) {
    //      int lower = getContent().getLineAtOffset(globalOffsetRange.lowerEndpoint().intValue());
    //      int upper = getContent().getLineAtOffset(globalOffsetRange.upperEndpoint().intValue());
    //      return Range.closed(Integer.valueOf(lower), Integer.valueOf(upper));
    //   }

    private void bindSelectionListener() {
        this.textSelection.addListener((x, o, n) -> {
            RangeSet<Integer> toUpdate = TreeRangeSet.create();
            if (o != null)
                toUpdate.add(getAffectedLines(o));
            if (n != null)
                toUpdate.add(getAffectedLines(n));
            updateNodesNow(toUpdate);
        });
    }

    private void bindContentListener() {
        this.content.addListener((x, o, n) -> {
            if (o != null) {
                o.removeTextChangeListener(this.textChangeListener);
            }
            if (n != null) {
                n.addTextChangeListener(this.textChangeListener);
                this.numberOfLines.set(n.getLineCount());
            }
        });
        StyledTextContent current = this.content.get();
        if (current != null) {
            current.addTextChangeListener(this.textChangeListener);

            // set inital values
            this.numberOfLines.set(current.getLineCount());

        }
    }

    private TextChangeListener textChangeListener = new TextChangeListener() {

        private Function<Integer, Integer> mapping;
        private RangeSet<Integer> toUpdate = TreeRangeSet.create();
        private RangeSet<Integer> toRelease = TreeRangeSet.create();

        @Override
        public void textSet(TextChangedEvent event) {
            if (!this.toUpdate.isEmpty()) {
                updateNodesNow(this.toUpdate);
                this.toUpdate.clear();
            }
            // update number of lines
            ContentView.this.numberOfLines.set(getContent().getLineCount());

            getLineLayer().requestLayout();
        }

        private int computeFirstUnchangedLine(TextChangingEvent event) {

            int endOffset = event.offset + event.replaceCharCount;
            int endLineIndex = getContent().getLineAtOffset(endOffset);
            int endLineBegin = getContent().getOffsetAtLine(endLineIndex);
            //         int endLineLength = ContentView.this.lineHelper.getLength(endLineIndex);

            int firstSafeLine;

            if (endLineBegin == event.offset) {
                // offset at beginning of line
                firstSafeLine = endLineIndex;
            } else {
                // offset in middle or at end of line
                firstSafeLine = endLineIndex + 1;
            }

            return firstSafeLine;
        }

        @Override
        public void textChanging(TextChangingEvent event) {
            final int changeBeginLine = getContent().getLineAtOffset(event.offset);

            // determine first unchanged line
            int firstUnchangedLine = computeFirstUnchangedLine(event);

            int deltaLines = event.newLineCount - event.replaceLineCount;

            if (deltaLines < 0) {
                this.toRelease.add(Range.closedOpen(Integer.valueOf(firstUnchangedLine + deltaLines),
                        Integer.valueOf(firstUnchangedLine)));
            }

            // prepare permutation
            this.mapping = (idx) -> {
                if (idx.intValue() >= firstUnchangedLine) {
                    return Integer.valueOf(idx.intValue() + deltaLines);
                }
                return idx;
            };

            this.toUpdate.add(Range.closedOpen(Integer.valueOf(changeBeginLine),
                    Integer.valueOf(firstUnchangedLine + deltaLines)));
            // At least update myself
            if (this.toUpdate.isEmpty()) {
                this.toUpdate.add(Range.closed(Integer.valueOf(changeBeginLine), Integer.valueOf(changeBeginLine)));
            }

            //
            //         // simple insert
            //         if (event.replaceCharCount == 0) {
            //            if (event.newLineCount > 0) {
            //
            //               int lineIndex = getContent().getLineAtOffset(event.offset);
            //               int lineBegin = getContent().getOffsetAtLine(lineIndex);
            //               int lineLength = lineHelper.getLength(lineIndex);
            //
            //               int firstSafeLine;
            //               Range<Integer> updateRange;
            //
            //               if (lineBegin == event.offset) {
            //                  // at beginning of line
            //                  firstSafeLine = lineIndex;
            //                  updateRange = Range.closedOpen(lineIndex, lineIndex + event.newLineCount);
            //               }
            //               else if (lineBegin == event.offset + lineLength) {
            //                  // insert was at end of line
            //                  firstSafeLine= lineIndex + 1;
            //                  updateRange = Range.closedOpen(lineIndex, lineIndex + event.newLineCount);
            //               }
            //               else {
            //                  // insert was in middle of line
            //                  firstSafeLine = lineIndex + 2;
            //                  updateRange = Range.closedOpen(lineIndex, lineIndex + 1 + event.newLineCount);
            //               }
            //
            //               // prepare update
            //               toUpdate.add(updateRange);
            //
            //               // prepare permutation
            //               this.mapping = (idx) -> {
            //                  if (idx >= firstSafeLine) {
            //                     return idx + event.newLineCount;
            //                  }
            //                  return idx;
            //               };
            //
            //            }
            //         }

            //         int firstUnchangedLine = changeBeginLine + replaceLines;
            //
            //         int newFirstUnchnagedLine = changeBeginLine + newLines;
            //
            //
            //         // prepare updates
            //         toUpdate.add(Range.closedOpen(changeBeginLine, changeBeginLine + newLines));
            //         // prepare permutation
            //         this.mapping = (idx) -> {
            //            if (idx >= firstUnchangedLine) {
            //               return idx + firstUnchangedLine - newFirstUnchnagedLine;
            //            }
            //            return idx;
            //         };

        }

        @Override
        public void textChanged(TextChangedEvent event) {
            if (!this.toRelease.isEmpty()) {
                releaseNodesNow(this.toRelease);
                this.toRelease.clear();
            }

            // execute permutation
            if (this.mapping != null) {
                getLineLayer().permutateNodes(this.mapping);
                this.mapping = null;
            }

            // execute updates
            if (!this.toUpdate.isEmpty()) {
                updateNodesNow(this.toUpdate);
                this.toUpdate.clear();
            }

            // update number of lines
            ContentView.this.numberOfLines.set(getContent().getLineCount());

            getLineLayer().requestLayout();
        }
    };

    //   Timer t = new Timer();
    //   volatile boolean scheduled = false;
    //   private void scheduleUpdate() {
    //      if (true) return;
    //
    //      try {
    //         if (!scheduled) {
    //            scheduled = true;
    //            t.schedule(new TimerTask() {
    //               @Override
    //               public void run() {
    //                  Platform.runLater(()->doUpdate());
    //                  scheduled = false;
    //               }
    //            }, 16);
    //         }
    //      }
    //      catch (Exception e) {
    //         e.printStackTrace();
    //      }
    //
    //   }

    private void onLineChange(Observable o) {
        RangeSet<Integer> toUpdate = TreeRangeSet.create();
        RangeSet<Integer> toRelease = TreeRangeSet.create();

        double offsetY = offsetYProperty().get();
        com.google.common.collect.Range<Integer> visibleLines = visibleLinesProperty().get();
        ContiguousSet<Integer> set = ContiguousSet.create(visibleLines, DiscreteDomain.integers());
        double lineHeight = lineHeightProperty().get();

        // schedule visible line updates
        if (this.curVisibleLines == null) {
            toUpdate.add(visibleLines);
        } else {
            RangeSet<Integer> hiddenLines = TreeRangeSet.create();
            hiddenLines.add(this.curVisibleLines);
            hiddenLines.remove(visibleLines);

            RangeSet<Integer> shownLines = TreeRangeSet.create();
            shownLines.add(visibleLines);
            shownLines.remove(this.curVisibleLines);

            toUpdate.addAll(shownLines);
            toRelease.addAll(hiddenLines);
        }
        this.curVisibleLines = visibleLines;

        // store precomputed y data
        for (int index : set) {
            double y = index * lineHeight - offsetY;
            this.yOffsetData.put(Integer.valueOf(index), Double.valueOf(y));
            //         this.forceLayout = true;
        }

        releaseNodesNow(toRelease);
        updateNodesNow(toUpdate);

        this.lineLayer.requestLayout();
        //      scheduleUpdate();
    }

    private double cachedLongestLine;
    private int lastContentLength;

    private double computeLongestLine() {
        if (this.cachedLongestLine != 0.0) {
            if (this.lastContentLength == getContent().getCharCount()) {
                return Math.max(this.cachedLongestLine, getWidth());
            }
        }

        OptionalInt longestLine = IntStream.range(0, getNumberOfLines())
                .map(index -> this.lineHelper.getLengthCountTabsAsChars(index)).max();
        if (longestLine.isPresent()) {
            int lineLength = longestLine.getAsInt() + 2;
            this.cachedLongestLine = lineLength * getCharWidth();
            this.lastContentLength = getContent().getCharCount();
            return Math.max(getWidth(), this.cachedLongestLine);
        } else {
            this.cachedLongestLine = 0.0;
        }

        return getWidth();
    }

    public double getCharWidth() {
        if (!this.skipCSSApply) {
            applyCss();
            if (getParent() != null) {
                this.skipCSSApply = true;
            }
        }

        return this.charWidth.get();
    }

    @Override
    protected void layoutChildren() {
        double scrollX = -this.offsetX.get();
        if (Double.isNaN(scrollX)) {
            scrollX = 0.0;
        }
        this.contentBody.resizeRelocate(scrollX, 0, computeLongestLine(), getHeight());
    }

    private DoubleProperty offsetY = new SimpleDoubleProperty();
    private DoubleProperty offsetX = new SimpleDoubleProperty();

    public void bindHorizontalScrollbar(ScrollBar bar) {
        bar.setMin(0);
        DoubleBinding max = this.contentBody.widthProperty().subtract(widthProperty());
        DoubleBinding factor = this.contentBody.widthProperty().divide(max);

        //      DoubleProperty santizedFactor = new SimpleDoubleProperty();
        //      santizedFactor.set( Double.isNaN(factor.get()) ? 1.0 : factor.get() );
        //      factor.addListener( (ob,ol,ne) -> {
        //         santizedFactor.set( Double.isNaN(ne.doubleValue()) ? 1.0 : ne.doubleValue() );
        //      } );

        maxValue = this.contentBody.widthProperty().divide(factor);
        visibleAmount = widthProperty().divide(factor);

        updateValue(maxValue.get(), bar.maxProperty());
        updateValue(visibleAmount.get(), bar.visibleAmountProperty());

        maxValue.addListener(o -> {
            updateValue(maxValue.get(), bar.maxProperty());
        });
        visibleAmount.addListener(o -> {
            updateValue(visibleAmount.get(), bar.visibleAmountProperty());
        });

        //      bar.maxProperty().bind(maxValue);
        //      bar.visibleAmountProperty().bind(visibleAmount);

        this.offsetX.bind(bar.valueProperty());

        this.widthProperty().addListener((x, o, n) -> {
            if (!Double.isNaN(bar.getMax()) && !Double.isNaN(bar.getValue())) {
                bar.setValue(Math.max(0, Math.min(bar.getMax(), bar.getValue())));
            }
        });
    }

    private void updateValue(double v, DoubleProperty p) {
        if (Double.isNaN(v)) {
            p.set(0);
        } else {
            p.set(v);
        }
    }

    public void bindVerticalScrollbar(ScrollBar bar) {
        this.heightProperty().addListener((x, o, n) -> {
            if (!Double.isNaN(bar.getMax()) && !Double.isNaN(bar.getValue())) {
                bar.setValue(Math.max(0, Math.min(bar.getMax(), bar.getValue())));
            }
        });
    }

    public DoubleProperty offsetYProperty() {
        return this.offsetY;
    }

    //   private RangeSet<Integer> toRelease = TreeRangeSet.create();
    //   private RangeSet<Integer> toUpdate = TreeRangeSet.create();

    public void updatelines(com.google.common.collect.RangeSet<Integer> rs) {
        updateNodesNow(rs);
    }

    private int insertionMarkerIndex = -1;

    private DoubleBinding maxValue;

    private DoubleBinding visibleAmount;

    public void updateInsertionMarkerIndex(int index) {
        if (this.insertionMarkerIndex != index) {
            this.insertionMarkerIndex = index;
        }
        com.google.common.collect.RangeSet<Integer> rs = TreeRangeSet.create();
        updateNodesNow(rs.complement());
    }

    void updateNodesNow(com.google.common.collect.RangeSet<Integer> rs) {
        RangeSet<Integer> subRangeSet = rs.subRangeSet(getVisibleLines())
                .subRangeSet(Range.closedOpen(Integer.valueOf(0), Integer.valueOf(getNumberOfLines())));
        subRangeSet.asRanges().forEach(r -> {
            ContiguousSet.create(r, DiscreteDomain.integers()).forEach(index -> {
                getLineLayer().updateNode(index.intValue());
                //            StyledTextLine m = this.model.get(index);
                //            lineLayer.updateNode(m);
            });
        });
    }

    void releaseNodesNow(com.google.common.collect.RangeSet<Integer> rs) {
        RangeSet<Integer> subRangeSet = rs
                .subRangeSet(Range.closedOpen(Integer.valueOf(0), Integer.valueOf(getNumberOfLines())));
        subRangeSet.asRanges().forEach(r -> {
            ContiguousSet.create(r, DiscreteDomain.integers()).forEach(index -> {
                getLineLayer().releaseNode(index.intValue());
                //            StyledTextLine m = this.model.get(index);
                //            lineLayer.releaseNode(m);
            });
        });
    }

    //   private void updateNodes(com.google.common.collect.Range<Integer> range) {
    //      toUpdate.add(range);
    //      scheduleUpdate();
    ////
    ////      if (range.intersects(getVisibleLines())) {
    ////         Range intersection = range.intersect(getVisibleLines());
    ////         for (int index = intersection.getOffset(); index <intersection.getEndOffset(); index++) {
    ////            toUpdate.add(index);
    //////            StyledTextLine m = this.model.get(index);
    //////            prepareNode(index, m);
    ////         }
    ////      }
    //   }

    //   private boolean doUpdate() {
    //      try {
    //         long now = -System.nanoTime();
    //         if (!toRelease.isEmpty()) {
    //            toRelease.asRanges().forEach(r-> {
    //               ContiguousSet.create(r, DiscreteDomain.integers()).forEach(this.lineLayer::releaseNode);
    //            });
    //            toRelease.clear();
    //         }
    //
    //         if (!toUpdate.isEmpty()) {
    //            toUpdate.subRangeSet(getVisibleLines()).subRangeSet(Range.closedOpen(0, this.model.size())).asRanges().forEach(r-> {
    //               ContiguousSet.create(r, DiscreteDomain.integers()).forEach(index-> {
    //                  StyledTextLine m = this.model.get(index);
    //                  lineLayer.updateNode(index, m);
    //               });
    //            });
    //            toUpdate.clear();
    //         }
    //
    //
    //         now += System.nanoTime();
    //
    //         if (now > 1000_000 * 5) {
    //         }
    //
    //         if (!toRelease.isEmpty() || !toUpdate.isEmpty() || forceLayout) {
    //
    //             lineLayer.requestLayout();
    //
    //            forceLayout = false;
    //
    //            return true;
    //         }
    //      }
    //      catch (Exception e) {
    //         e.printStackTrace();
    //      }
    //      return false;
    //
    //   }
    //
    //   private void updatePulse(long now) {
    //      doUpdate();
    //   }

    public Optional<Point2D> getLocationInScene(int globalOffset, LineLocation locationHint) {
        applyCss();
        layout();

        int lineIndex = getContent().getLineAtOffset(globalOffset);
        Optional<LineNode> node = this.lineLayer.getVisibleNode(lineIndex);

        return node.map(n -> {
            double x = n.getCharLocation(globalOffset - n.getStartOffset());
            double y = 0;
            switch (locationHint) {
            case BELOW:
                y = 0;
                break;
            case ABOVE:
                y = -getLineHeight();
                break;
            case CENTER:
                y = -getLineHeight() / 2.0;
                break;
            }
            Point2D p = new Point2D(x, y);
            return n.localToScene(p);
        });
    }

    public Optional<Integer> getLineIndex(Point2D point) {
        // transform point to respect horizontal scrolling
        Point2D p = this.lineLayer.sceneToLocal(this.localToScene(point));
        Optional<Integer> result = this.lineLayer.getLineIndex(p);
        return result;

    }

    public void updateAnnotations(RangeSet<Integer> r) {
        updateNodesNow(r);
    }

}