Skip to main content
summaryrefslogblamecommitdiffstats
blob: 1d804b088b7456c9e68e2f99d097aaa84d73c6c1 (plain) (tree)
1
2
3
4
5
6
7
8
9
10









                                                                                  




                                                        
                      
                           
                                    
 



                                                                        
                                           
                                            
 

                                              
                                           

                                          
                               











                                                  
                               


                                         
                             

                                     

                                  
                                                                                                                                                                                                 
















                                                                                                                                        















                                                                           

























                                                                              

                                                                                                                               
 



                                                                                                                                          
 






































                                                                                                                                                                                                                                                                           





















                                                                                                                                                                                                                               
                                                    












                                                                                        
                                                                                                                                                                                                                          
 







                                                                                                          
 





                                                                            
 








                                                                                               

















































                                                                                                                                                      

                                                       



                                                                                      
                                                 

                                                 
                                                 











                                                                       
                                                


                                                                       
                                             



                                                                             










                                                                                   








                                                    
                                                      


                                                                  
                                                                 
                                                       


















                                                                                  








                                                                                                                                                                  























































                                                                                  


                                                 












                                                         



                                                                                        






































































































                                                                                                                                       
/*******************************************************************************
 * 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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.eclipse.fx.core.Subscription;
import org.eclipse.fx.core.Util;
import org.eclipse.fx.ui.controls.styledtext.DecorationStrategyFactory;
import org.eclipse.fx.ui.controls.styledtext.model.DecorationStrategy;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.ParsedValue;
import javafx.css.SimpleStyleableIntegerProperty;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.StyleConverter;
import javafx.css.Styleable;
import javafx.css.StyleableProperty;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextBoundsType;

@SuppressWarnings("javadoc")
public class TextNode extends HBox {

	@SuppressWarnings("null")
	@NonNull
	private static final CssMetaData<TextNode, @NonNull Paint> FILL = new CssMetaData<TextNode, @NonNull Paint>("-fx-fill", StyleConverter.getPaintConverter(), Color.BLACK) { //$NON-NLS-1$

		@Override
		public boolean isSettable(TextNode styleable) {
			return !styleable.fillProperty().isBound();
		}

		@SuppressWarnings("unchecked")
		@Override
		public StyleableProperty<@NonNull Paint> getStyleableProperty(TextNode styleable) {
			return (StyleableProperty<@NonNull Paint>) styleable.fill;
		}

	};
	@SuppressWarnings("null")
	@NonNull
	final ObjectProperty<@NonNull Paint> fill = new SimpleStyleableObjectProperty<>(FILL, this, "fill", Color.BLACK); //$NON-NLS-1$

	private static String[] BASIC_STRING_CACHE = new String[256];

	private static String toString(char c) {
		String rv = null;
		if( c < BASIC_STRING_CACHE.length ) {
			rv = BASIC_STRING_CACHE[c];
			if( rv == null ) {
				BASIC_STRING_CACHE[c] = String.valueOf(c);
			}
		}
		if( rv == null ) {
			rv = String.valueOf(c);
		}
		return rv;
	}

	/**
	 * The paint used to fill the text
	 *
	 * @return the property to observe
	 */
	public final @NonNull ObjectProperty<@NonNull Paint> fillProperty() {
		return (ObjectProperty<@NonNull Paint>) this.fill;
	}

	/**
	 * @return the paint used to fill the text
	 */
	public final @NonNull Paint getFill() {
		return this.fillProperty().get();
	}

	/**
	 * Set a new paint
	 *
	 * @param color
	 *            the paint used to fill the text
	 */
	public final void setFill(final @NonNull Paint color) {
		this.fillProperty().set(color);
	}

	static class DecorationStyleConverter extends StyleConverter<ParsedValue<?, DecorationStrategy>, DecorationStrategy> {
		private static Map<String, DecorationStrategyFactory> FACTORIES;

		@SuppressWarnings("null")
		@Override
		public DecorationStrategy convert(ParsedValue<ParsedValue<?, DecorationStrategy>, DecorationStrategy> value, Font font) {
			String definition = value.getValue() + ""; //$NON-NLS-1$

			if (FACTORIES == null) {
				FACTORIES = Util.lookupServiceList(getClass(), DecorationStrategyFactory.class).stream().sorted((f1, f2) -> -1 * Integer.compare(f1.getRanking(), f2.getRanking())).collect(Collectors.toMap(f -> f.getDecorationStrategyName(), f -> f));
			}

			String type;
			if (definition.contains("(")) { //$NON-NLS-1$
				type = definition.substring(0, definition.indexOf('('));
			} else {
				type = definition + ""; //$NON-NLS-1$
			}

			DecorationStrategyFactory strategy = FACTORIES.get(type);
			if (strategy != null) {
				return strategy.create(definition.contains("(") ? definition.substring(definition.indexOf('(') + 1, definition.lastIndexOf(')')) : null); //$NON-NLS-1$
			}

			return null;
		}
	}

	@NonNull
	private static final DecorationStyleConverter CONVERTER = new DecorationStyleConverter();

	@SuppressWarnings("null")
	@NonNull
	private static final CssMetaData<TextNode, @Nullable DecorationStrategy> DECORATIONSTRATEGY = new CssMetaData<TextNode, @Nullable DecorationStrategy>("-efx-decoration", CONVERTER, null) { //$NON-NLS-1$

		@Override
		public boolean isSettable(TextNode node) {
			return !node.decorationStrategyProperty().isBound();
		}

		@SuppressWarnings("unchecked")
		@Override
		public StyleableProperty<@Nullable DecorationStrategy> getStyleableProperty(TextNode node) {
			return (StyleableProperty<@Nullable DecorationStrategy>) node.decorationStrategyProperty();
		}

	};

	@SuppressWarnings("null")
	@NonNull
	private static final CssMetaData<TextNode, @NonNull Number> TABCHARADANCE = new CssMetaData<TextNode, @NonNull Number>("-efx-tab-char-advance", StyleConverter.getSizeConverter(), Integer.valueOf(4)) { //$NON-NLS-1$

		@Override
		public boolean isSettable(TextNode styleable) {
			return !styleable.tabCharAdvanceProperty().isBound();
		}

		@SuppressWarnings("unchecked")
		@Override
		public StyleableProperty<@NonNull Number> getStyleableProperty(TextNode styleable) {
			return (StyleableProperty<@NonNull Number>) styleable.tabCharAdvance;
		}

	};

	private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;

	static {
		final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData());
		styleables.add(DECORATIONSTRATEGY);
		styleables.add(FILL);
		STYLEABLES = Collections.unmodifiableList(styleables);
	}

	public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
		return STYLEABLES;
	}

	@Override
	public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
		return getClassCssMetaData();
	}

	private @NonNull ObjectProperty<@Nullable DecorationStrategy> decorationStrategy = new SimpleStyleableObjectProperty<@Nullable DecorationStrategy>(DECORATIONSTRATEGY, this, "decorationStrategy"); //$NON-NLS-1$

	/**
	 * The current strategy used for decoration
	 *
	 * @return the property to observe
	 */
	public final @NonNull ObjectProperty<@Nullable DecorationStrategy> decorationStrategyProperty() {
		return this.decorationStrategy;
	}

	/**
	 * @return strategy used for decoration
	 */
	public final @Nullable DecorationStrategy getDecorationStrategy() {
		return this.decorationStrategyProperty().get();
	}

	/**
	 * Set a new strategy used for decoration
	 *
	 * @param strategy
	 *            the strategy
	 */
	public final void setDecorationStrategy(final @Nullable DecorationStrategy strategy) {
		this.decorationStrategyProperty().set(strategy);
	}

	@NonNull
	IntegerProperty tabCharAdvance = new SimpleStyleableIntegerProperty(TABCHARADANCE, this, "tabCharAdvance", Integer.valueOf(4)); //$NON-NLS-1$

	/**
	 * Number of chars to use for tab advance (default is 4)
	 *
	 * @return the property to observe
	 */
	public final IntegerProperty tabCharAdvanceProperty() {
		return this.tabCharAdvance;
	}

	/**
	 * @return the number of chars to use for tab advance (default is 4)
	 */
	public final int getTabCharAdvance() {
		return this.tabCharAdvanceProperty().get();
	}

	/**
	 * Set a new number for chars to advance for a tab
	 *
	 * @param tabCharAdvance
	 *            the number of chars to use for tab advance (default is 4)
	 */
	public final void setTabCharAdvance(final int tabCharAdvance) {
		this.tabCharAdvanceProperty().set(tabCharAdvance);
	}


//	private ReuseCache<Text> letterCache = new ReuseCache<Text>(this::createText);

	private int startOffset;
	private List<Integer> tabPositions = new ArrayList<>();
	private String originalText;
//	private final HBox textNode;
//	private final Text textNode;

	private List<Text> activeLetters = new ArrayList<>();
	private ReuseCache<Text> cache;


	/**
	 * Create a new styled text node
	 *
	 * @param text
	 *            the text
	 */
	public TextNode(String text) {
		setMinWidth(Region.USE_COMPUTED_SIZE);
		this.cache = new ReuseCache<>(()->{
			Text letter = new Text();
			letter.setBoundsType(TextBoundsType.LOGICAL_VERTICAL_CENTER);
			return letter;
		});
		this.cache.addOnActivate(node->{
			getChildren().add(node);
		});
		this.cache.addOnRelease(node-> {
			getChildren().remove(node);
		});
//		cache.addOnClear(node->getChildren().remove(node));

		getStyleClass().add("styled-text-node"); //$NON-NLS-1$
		this.originalText = text;

		rebuildText(text);

		this.tabCharAdvance.addListener(o -> {
			rebuildText(text);
		});
		this.cache.addOnActivate((n)->{
			((Text)n).fillProperty().bind(fillProperty());

		});
		this.cache.addOnRelease(n->{
			((Text)n).fillProperty().unbind();
		});

//		setStyle("-fx-border-width: 0.1px; -fx-border-color: blue");


		decorationStrategy.addListener((x, o, n)->{
			System.err.println("deco start changed: " + n);
		});

		this.decorationStrategy.addListener(this::handleDecorationChange);
	}

	public Subscription setStyledRange(String clazz, int start, int end) {
		return null;
	}

	public void updateText(String text) {
		rebuildText(text);
		this.originalText = text;
	}

	private void rebuildText(String text) {
		for (Text t : this.activeLetters) {
			this.cache.releaseElement(t);
		}
		this.activeLetters.clear();
		for( char c : processText(text).toCharArray() ) {
			Text textNode = this.cache.getElement();
			textNode.setText(toString(c));
			this.activeLetters.add(textNode);
		}
	}

	private String processText(String text) {
		String tmp = text;
		StringBuilder b = new StringBuilder();
		for (int i = 0; i < this.tabCharAdvance.get(); i++) {
			b.append(" "); //$NON-NLS-1$
		}
		int position = -1;
		this.tabPositions.clear();
		while ((position = tmp.indexOf('\t')) != -1) {
			tmp = tmp.replaceFirst("\t", b.toString()); //$NON-NLS-1$
			this.tabPositions.add(Integer.valueOf(position));
		}
		return tmp;
	}

	private void handleDecorationChange(ObservableValue<? extends DecorationStrategy> observable, DecorationStrategy oldValue, DecorationStrategy newValue) {
		if (oldValue != null) {
			oldValue.unattach(this, this);
		}

		if (newValue != null) {
			newValue.attach(this, this);
		}
	}

	void setStartOffset(int startOffset) {
		this.startOffset = startOffset;
	}

	/**
	 * @return the start offset in the file
	 */
	@Deprecated
	public int getStartOffset() {
		return this.startOffset;
	}

	/**
	 * @return the end offset in the file
	 */
	@Deprecated
	public int getEndOffset() {
		return this.startOffset + getText().length();
	}

	/**
	 * Check if the text node intersects with the start and end locations
	 *
	 * @param start
	 *            the start
	 * @param end
	 *            the end
	 * @return <code>true</code> if intersects
	 */
	public boolean intersectOffset(int start, int end) {
		if (getStartOffset() > end) {
			return false;
		} else if (getEndOffset() < start) {
			return false;
		}
		return true;
	}

	@Override
	public String toString() {
		return "'" + this.originalText + "'";  //$NON-NLS-1$//$NON-NLS-2$
	}

	@Override
	public ObservableList<Node> getChildren() {
		return super.getChildren();
	}

	/**
	 * @return the text
	 */
	public String getText() {
		return this.originalText;
	}

//	protected double getFixedLetterWidth() {
//		return 8;
//	}

	@Override
	protected void layoutChildren() {
		super.layoutChildren();

//		double off = 0;
//
//		for (Text letter : this.activeLetters) {
//			letter.setLayoutX(off);
//			off += getFixedLetterWidth();
//		}

//		this.textNode.relocate(0, 0);
		DecorationStrategy decorationStrategy2 = this.decorationStrategy.get();
		if (decorationStrategy2 != null) {
			decorationStrategy2.layout(this, this);
		}
	}

	/**
	 * Get the caret index at the given point
	 *
	 * @param point
	 *            the point relative to this node
	 * @return the index or <code>-1</code>
	 */
	public int getCaretIndexAtPoint(Point2D point) {
		Point2D local = this.sceneToLocal(localToScene(point));
//		System.err.println(local);

		Optional<Node> charNode = this.getChildren().stream().filter( n -> n.getBoundsInParent().contains(local)).findFirst();
		if( charNode.isPresent() ) {
			Node node = charNode.get();
			int idx = this.getChildren().indexOf(node);
			Bounds bounds = node.getBoundsInParent();

			// if it is the 2nd half of the character
			if( bounds.getMinX() + bounds.getWidth() / 2 < local.getX() ) {
				idx += 1;
			}

			int toRemove = 0;
			for (Integer i : this.tabPositions) {
				if (i.intValue() <= idx && idx < i.intValue() + this.tabCharAdvance.get()) {
					toRemove += idx - i.intValue();
					// If we are in the 2nd half of the tab we
					// simply move one past the value
					if ((idx - i.intValue()) % this.tabCharAdvance.get() >= this.tabCharAdvance.get() / 2) {
						idx += 1;
					}
					break;
				} else if (i.intValue() < idx) {
					toRemove += this.tabCharAdvance.get() - 1;
				}
			}
			idx -= toRemove;
//			System.err.println("==> (TextNode)" + idx);
			return idx;
		} else {
//			System.err.println("COULD NOT FIND NODE AT: " + local);
//			this.textNode.getChildren().stream().map( n -> n.getBoundsInParent()).forEach(System.err::println);
		}

//		@SuppressWarnings("deprecation")
//		HitInfo info = this.textNode.impl_hitTestChar(this.textNode.sceneToLocal(localToScene(point)));
//		if (info != null) {
//			int idx = info.getInsertionIndex();
//			int toRemove = 0;
//			for (Integer i : this.tabPositions) {
//				if (i.intValue() <= idx && idx < i.intValue() + this.tabCharAdvance.get()) {
//					toRemove += idx - i.intValue();
//					// If we are in the 2nd half of the tab we
//					// simply move one past the value
//					if ((idx - i.intValue()) % this.tabCharAdvance.get() >= this.tabCharAdvance.get() / 2) {
//						idx += 1;
//					}
//					break;
//				} else if (i.intValue() < idx) {
//					toRemove += this.tabCharAdvance.get() - 1;
//				}
//			}
//			idx -= toRemove;
//			return idx;
//		}
		return -1;
	}

	/**
	 * Find the x coordinate where we the character for the given index starts
	 *
	 * @param index
	 *            the index
	 * @return the location or <code>0</code> if not found
	 */
	public double getCharLocation(int index) {
		int realIndex = index;
		for (Integer i : this.tabPositions) {
			if (i.intValue() < realIndex) {
				realIndex += this.tabCharAdvance.get() - 1;
			}
		}

		if( realIndex >= 0 && realIndex < this.getChildren().size() ) {
			return this.localToParent(this.getChildren().get(realIndex).getBoundsInParent()).getMinX();
		} else if( ! this.getChildren().isEmpty() ) {
			return this.localToParent(this.getChildren().get(this.getChildren().size()-1).getBoundsInParent()).getMaxX();
		}

		return 0.0;

//		this.textNode.setImpl_caretPosition(realIndex);
//		PathElement[] pathElements = this.textNode.getImpl_caretShape();
//		for (PathElement e : pathElements) {
//			if (e instanceof MoveTo) {
//				return this.textNode.localToParent(((MoveTo) e).getX(), 0).getX();
//			}
//		}

	}
}

Back to the top