blob: 7d8c85b0af8557698f9f7fd0ec09422d55da8c67 [file] [log] [blame]
/*******************************************************************************
* 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", new TextSelection(0, 0)); //$NON-NLS-1$
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", Range.closed(Integer.valueOf(0), Integer.valueOf(0))); //$NON-NLS-1$
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(), this.area.fontZoomFactorProperty()); //$NON-NLS-1$
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);
}
}