org.eclipse.fx.ui.controls.styledtext.StyledTextNode Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of org.eclipse.fx.ui.controls Show documentation
Show all versions of org.eclipse.fx.ui.controls Show documentation
Custom JavaFX controls like a styled text component, ...
/*******************************************************************************
* Copyright (c) 2015 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 - initial API and implementation
*******************************************************************************/
package org.eclipse.fx.ui.controls.styledtext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
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.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.PathElement;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextBoundsType;
import org.eclipse.fx.core.Util;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import com.sun.javafx.css.converters.PaintConverter;
import com.sun.javafx.scene.text.HitInfo;
/**
* A node who allows to decorate the text
*
* @since 2.0
*/
@SuppressWarnings("restriction")
public class StyledTextNode extends Region {
/**
* A strategy to decorate the text
*/
public interface DecorationStrategy {
/**
* Attach the decoration on the text
*
* @param node
* the node the decoration is attached to
* @param textNode
* the text node decorated
*/
public void attach(StyledTextNode node, Text textNode);
/**
* Remove the decoration from the text
*
* @param node
* the node the decoration is attached to
* @param textNode
* the text node decorated
*/
public void unattach(StyledTextNode node, Text textNode);
/**
* Layout the decoration
*
* @param node
* the node the layout pass is done on
* @param textNode
* the text node decorated
*/
public void layout(StyledTextNode node, Text textNode);
}
private final Text textNode;
@SuppressWarnings("null")
@NonNull
private static final CssMetaData FILL = new CssMetaData("-fx-fill", PaintConverter.getInstance(), Color.BLACK) { //$NON-NLS-1$
@Override
public boolean isSettable(StyledTextNode styleable) {
return !styleable.fillProperty().isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty<@NonNull Paint> getStyleableProperty(StyledTextNode 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$
/**
* 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, DecorationStrategy> {
private static Map FACTORIES;
@SuppressWarnings("null")
@Override
public DecorationStrategy convert(ParsedValue, 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 DECORATIONSTRATEGY = new CssMetaData("-efx-decoration", CONVERTER, null) { //$NON-NLS-1$
@Override
public boolean isSettable(StyledTextNode node) {
return !node.decorationStrategyProperty().isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty<@Nullable DecorationStrategy> getStyleableProperty(StyledTextNode node) {
return (StyleableProperty<@Nullable DecorationStrategy>) node.decorationStrategyProperty();
}
};
@SuppressWarnings("null")
@NonNull
private static final CssMetaData TABCHARADANCE = new CssMetaData("-efx-tab-char-advance", StyleConverter.getSizeConverter(), Integer.valueOf(4)) { //$NON-NLS-1$
@Override
public boolean isSettable(StyledTextNode styleable) {
return !styleable.tabCharAdvanceProperty().isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty<@NonNull Number> getStyleableProperty(StyledTextNode styleable) {
return (StyleableProperty<@NonNull Number>) styleable.tabCharAdvance;
}
};
private static final List> STYLEABLES;
static {
final List> styleables = new ArrayList>(Region.getClassCssMetaData());
styleables.add(DECORATIONSTRATEGY);
styleables.add(FILL);
STYLEABLES = Collections.unmodifiableList(styleables);
}
public static List> getClassCssMetaData() {
return STYLEABLES;
}
@Override
public List> 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 int startOffset;
private List tabPositions = new ArrayList<>();
private String originalText;
/**
* Create a new styled text node
*
* @param text
* the text
*/
public StyledTextNode(String text) {
getStyleClass().add("styled-text-node"); //$NON-NLS-1$
this.originalText = text;
this.textNode = new Text(processText(text));
this.textNode.setBoundsType(TextBoundsType.LOGICAL_VERTICAL_CENTER);
this.textNode.fillProperty().bind(fillProperty());
getChildren().add(this.textNode);
this.decorationStrategy.addListener(this::handleDecorationChange);
this.tabCharAdvance.addListener(o -> {
this.textNode.setText(processText(text));
});
}
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.textNode);
}
if (newValue != null) {
newValue.attach(this, this.textNode);
}
}
void setStartOffset(int startOffset) {
this.startOffset = startOffset;
}
int getStartOffset() {
return this.startOffset;
}
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 true
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;
}
@Override
public ObservableList getChildren() {
return super.getChildren();
}
/**
* @return the text
*/
public String getText() {
return this.originalText;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
this.textNode.relocate(0, 0);
DecorationStrategy decorationStrategy2 = this.decorationStrategy.get();
if (decorationStrategy2 != null) {
decorationStrategy2.layout(this, this.textNode);
}
}
/**
* Get the caret index at the given point
*
* @param point
* the point relative to this node
* @return the index or -1
*/
public int getCaretIndexAtPoint(Point2D point) {
@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 0
if not found
*/
@SuppressWarnings("deprecation")
public double getCharLocation(int index) {
int realIndex = index;
for (Integer i : this.tabPositions) {
if (i.intValue() < realIndex) {
realIndex += this.tabCharAdvance.get() - 1;
}
}
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();
}
}
return 0.0;
}
}