package org.markdownwriterfx.editor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntFunction;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.PathElement;
import org.fxmisc.richtext.StyleClassedTextArea;
class ParagraphOverlayGraphicFactory
implements IntFunction<Node>
{
private final StyleClassedTextArea textArea;
private final List<OverlayFactory> overlayFactories = new ArrayList<>();
ParagraphOverlayGraphicFactory(StyleClassedTextArea textArea) {
this.textArea = textArea;
}
void addOverlayFactory(OverlayFactory overlayFactory) {
overlayFactories.add(overlayFactory);
update();
}
void removeOverlayFactory(OverlayFactory overlayFactory) {
overlayFactories.remove(overlayFactory);
update();
}
void update() {
IntFunction<? extends Node> factory = textArea.getParagraphGraphicFactory();
textArea.setParagraphGraphicFactory(null);
textArea.setParagraphGraphicFactory(factory);
}
@Override
public Node apply(int paragraphIndex) {
return overlayFactories.isEmpty() ? null : new ParagraphGraphic(paragraphIndex);
}
private class ParagraphGraphic
extends Pane
{
private final int paragraphIndex;
ParagraphGraphic(int paragraphIndex) {
this.paragraphIndex = paragraphIndex;
setPrefWidth(0);
setPrefHeight(0);
parentProperty().addListener((observable, oldParent, newParent) -> {
if (newParent != null && newParent.getChildrenUnmodifiable().get(0) != this) {
@SuppressWarnings("unchecked")
ObservableList<Node> children = (ObservableList<Node>) invoke(mGetChildren, newParent);
children.remove(this);
children.add(0, this);
}
});
}
@Override
protected void layoutChildren() {
update();
}
private void update() {
getChildren().clear();
if (getParent() == null)
return;
Node paragraphTextNode = getParent().lookup(".paragraph-text");
Insets insets = ((Region)paragraphTextNode).getInsets();
double leftInsets = insets.getLeft();
double topInsets = insets.getTop();
for (OverlayFactory overlayFactory : overlayFactories) {
overlayFactory.init(textArea, paragraphTextNode);
Node[] nodes = overlayFactory.createOverlayNodes(paragraphIndex);
if (nodes == null)
continue;
for (Node node : nodes) {
node.setManaged(false);
if (leftInsets != 0)
node.setLayoutX(node.getLayoutX() + leftInsets);
if (topInsets != 0)
node.setLayoutY(node.getLayoutY() + topInsets);
}
getChildren().addAll(nodes);
}
}
}
static abstract class OverlayFactory
{
private StyleClassedTextArea textArea;
private Node paragraphTextNode;
private void init(StyleClassedTextArea textArea, Node paragraphTextNode) {
this.textArea = textArea;
this.paragraphTextNode = paragraphTextNode;
}
abstract Node[] createOverlayNodes(int paragraphIndex);
protected StyleClassedTextArea getTextArea() {
return textArea;
}
protected PathElement[] getShape(int start, int end) {
return (PathElement[]) invoke(mGetRangeShape, paragraphTextNode, start, end);
}
protected Rectangle2D getBounds(int start, int end) {
PathElement[] shape = getShape(start, end);
double minX = 0, minY = 0, maxX = 0, maxY = 0;
for (PathElement pathElement : shape) {
if (pathElement instanceof MoveTo) {
MoveTo moveTo = (MoveTo) pathElement;
minX = maxX = moveTo.getX();
minY = maxY = moveTo.getY();
} else if (pathElement instanceof LineTo) {
LineTo lineTo = (LineTo) pathElement;
double x = lineTo.getX();
double y = lineTo.getY();
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
return new Rectangle2D(minX, minY, maxX - minX, maxY - minY);
}
}
private static Method mGetChildren;
private static Method mGetRangeShape;
static {
try {
mGetChildren = Parent.class.getDeclaredMethod("getChildren");
mGetChildren.setAccessible(true);
Class<?> textFlowExtClass = Class.forName("org.fxmisc.richtext.skin.TextFlowExt");
mGetRangeShape = textFlowExtClass.getDeclaredMethod("getRangeShape", int.class, int.class);
mGetRangeShape.setAccessible(true);
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException ex) {
throw new RuntimeException(ex);
}
}
private static Object invoke(Method m, Object obj, Object... args) {
try {
return m.invoke(obj, args);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new RuntimeException(ex);
}
}
}