Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
/*
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

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;

/**
 * A paragraph graphic factory for StyleClassedTextArea that is able to lay out
 * nodes over paragraph texts.
 *
 * Normally paragraph graphics are displayed left to the paragraph text.
 * E.g. used for line numbers. This factory creates a zero size graphic node,
 * which lays out (outside of its bounds) its unmanaged children over paragraph text.
 *
 * @author Karl Tauber
 */
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() {
		// temporary remove paragraph graphic factory to update the view
		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);
	}

	//---- class ParagraphGraphic ---------------------------------------------

	private class ParagraphGraphic
		extends Pane
	{
		private final int paragraphIndex;

		ParagraphGraphic(int paragraphIndex) {
			this.paragraphIndex = paragraphIndex;

			setPrefWidth(0);
			setPrefHeight(0);

			// make this node is the first child so that its nodes are rendered
			// 'under' the paragraph text
			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);
			}
		}
	}

	//---- class OverlayFactory -----------------------------------------------

	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);
		}
	}

	//---- reflection utilities -----------------------------------------------

	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);
		}
	}
}