/*******************************************************************************
* Copyright (c) 2014, 2016 itemis AG 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:
* Matthias Wienand (itemis AG) - initial API & implementation
*
*******************************************************************************/
package org.eclipse.gef.zest.fx.parts;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.eclipse.gef.fx.nodes.GeometryNode;
import org.eclipse.gef.fx.utils.NodeUtils;
import org.eclipse.gef.geometry.convert.fx.FX2Geometry;
import org.eclipse.gef.geometry.planar.Dimension;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.graph.Graph;
import org.eclipse.gef.mvc.fx.parts.AbstractContentPart;
import org.eclipse.gef.mvc.fx.parts.IResizableContentPart;
import org.eclipse.gef.mvc.fx.parts.ITransformableContentPart;
import org.eclipse.gef.mvc.fx.parts.IVisualPart;
import org.eclipse.gef.zest.fx.ZestProperties;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import javafx.collections.MapChangeListener;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.shape.StrokeType;
import javafx.scene.text.Text;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
/**
* The {@link NodePart} is the controller for a
* {@link org.eclipse.gef.graph.Node} content object.
*
* @author mwienand
*
*/
public class NodePart extends AbstractContentPart<Group>
implements ITransformableContentPart<Group>, IResizableContentPart<Group> {
/**
* JavaFX Node displaying a small icon representing a nested graph.
*/
public static class NestedGraphIcon extends Group {
{
Circle n0 = node(-20, -20);
Circle n1 = node(-10, 10);
Circle n2 = node(5, -15);
Circle n3 = node(15, -25);
Circle n4 = node(20, 5);
getChildren().addAll(edge(n0, n1), edge(n1, n2), edge(n2, n3), edge(n3, n4), edge(n1, n4), n0, n1, n2, n3,
n4);
}
private Node edge(Circle n, Circle m) {
Line line = new Line(n.getCenterX(), n.getCenterY(), m.getCenterX(), m.getCenterY());
line.setStroke(Color.BLACK);
return line;
}
private Circle node(double x, double y) {
return new Circle(x, y, 5, Color.BLACK);
}
}
// defaults
/**
* The default padding between the node's border and its content.
*/
protected static final double DEFAULT_SHAPE_PADDING = 5;
private static final String DEFAULT_SHAPE_ROLE = "defaultShape";
/**
* The zoom level that needs to be reached for the
* {@link #doGetContentChildren()} method to return a nested {@link Graph}.
*/
protected static final double ZOOMLEVEL_SHOW_NESTED_GRAPH = 2;
/**
* The default width of the nested graph area.
*/
protected static final double DEFAULT_CHILDREN_PANE_WIDTH = 300;
/**
* The default height of the nested graph area.
*/
protected static final double DEFAULT_CHILDREN_PANE_HEIGHT = 300;
/**
* The default zoom factor that is applied to the nested graph area.
*/
public static final double DEFAULT_NESTED_CHILDREN_ZOOM_FACTOR = 0.25;
/**
* The default width for the outer most layout container of this node in the
* case of nested content.
*/
public static final double DEFAULT_OUTER_LAYOUT_CONTAINER_WIDTH_NESTING = DEFAULT_CHILDREN_PANE_WIDTH
* DEFAULT_NESTED_CHILDREN_ZOOM_FACTOR;
/**
* The default height for the outer most layout container of this node in
* the case of nested content.
*/
public static final double DEFAULT_OUTER_LAYOUT_CONTAINER_HEIGHT_NESTING = DEFAULT_CHILDREN_PANE_HEIGHT
* DEFAULT_NESTED_CHILDREN_ZOOM_FACTOR;
// CSS classes for styling nodes
/**
* The CSS class that is applied to the {@link #getVisual() visual} of this
* {@link NodePart}.
*/
public static final String CSS_CLASS = "node";
/**
* The CSS class that is applied to the {@link Rectangle} that displays
* border and background.
*/
public static final String CSS_CLASS_SHAPE = "shape";
/**
* The CSS class that is applied to the {@link Text} that displays the
* label.
*/
public static final String CSS_CLASS_LABEL = "label";
/**
* The CSS class that is applied to the {@link Image} that displays the
* icon.
*/
public static final String CSS_CLASS_ICON = "icon";
private static final String NODE_LABEL_EMPTY = "";
private MapChangeListener<String, Object> nodeAttributesObserver = new MapChangeListener<String, Object>() {
@Override
public void onChanged(MapChangeListener.Change<? extends String, ? extends Object> change) {
refreshVisual();
}
};
private Text labelText;
private ImageView iconImageView;
private Tooltip tooltipNode;
private VBox vbox;
private Node shape;
private Node nestedGraphIcon;
private StackPane nestedContentStackPane;
private Pane nestedContentPane;
private AnchorPane nestedContentAnchorPane;
/**
* Creates the shape used to display the node's border and background.
*
* @return The newly created {@link Shape}.
*/
private Node createDefaultShape() {
GeometryNode<?> shape = new GeometryNode<>(new org.eclipse.gef.geometry.planar.Rectangle());
shape.setUserData(DEFAULT_SHAPE_ROLE);
shape.getStyleClass().add(CSS_CLASS_SHAPE);
shape.setFill(new LinearGradient(0, 0, 1, 1, true, CycleMethod.REFLECT,
Arrays.asList(new Stop(0, new Color(1, 1, 1, 1)))));
shape.setStroke(new Color(0, 0, 0, 1));
shape.setStrokeType(StrokeType.INSIDE);
return shape;
}
/**
* Creates the {@link Pane} that is used to display nested content.
*
* @return The {@link Pane} that is used to display nested content.
*/
private Pane createNestedContentPane() {
Pane nestedChildrenPaneScaled = new Pane();
Scale scale = new Scale();
nestedChildrenPaneScaled.getTransforms().add(scale);
scale.setX(DEFAULT_NESTED_CHILDREN_ZOOM_FACTOR);
scale.setY(DEFAULT_NESTED_CHILDREN_ZOOM_FACTOR);
return nestedChildrenPaneScaled;
}
@Override
protected void doActivate() {
super.doActivate();
getContent().attributesProperty().addListener(nodeAttributesObserver);
}
@Override
protected void doAddChildVisual(IVisualPart<? extends Node> child, int index) {
getNestedContentPane().getChildren().add(index, child.getVisual());
}
@Override
protected Group doCreateVisual() {
// container set-up
final Group group = new Group() {
@Override
public boolean isResizable() {
return true;
}
@Override
protected void layoutChildren() {
// we directly layout our children from within resize
};
@Override
public double maxHeight(double width) {
return vbox.maxHeight(width);
}
@Override
public double maxWidth(double height) {
return vbox.maxWidth(height);
}
@Override
public double minHeight(double width) {
return vbox.minHeight(width);
}
@Override
public double minWidth(double height) {
return vbox.minWidth(height);
}
@Override
public double prefHeight(double width) {
return vbox.prefHeight(width);
}
@Override
public double prefWidth(double height) {
return vbox.prefWidth(height);
}
@Override
public void resize(double w, double h) {
// for shape we use the exact size
shape.resize(w, h);
// for vbox we use the preferred size
vbox.setPrefSize(w, h);
vbox.autosize();
// and we relocate it to be horizontally and vertically centered
// w.r.t. the shape
Bounds vboxBounds = vbox.getLayoutBounds();
vbox.relocate((w - vboxBounds.getWidth()) / 2, (h - vboxBounds.getHeight()) / 2);
};
};
// create shape for border and background
shape = createDefaultShape();
// initialize image view
iconImageView = new ImageView();
iconImageView.setImage(null);
iconImageView.getStyleClass().add(CSS_CLASS_ICON);
// initialize text
labelText = new Text();
labelText.setText(NODE_LABEL_EMPTY);
labelText.getStyleClass().add(CSS_CLASS_LABEL);
HBox hbox = new HBox();
hbox.getChildren().addAll(iconImageView, labelText);
hbox.setAlignment(Pos.CENTER);
hbox.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
nestedContentPane = createNestedContentPane();
nestedContentStackPane = new StackPane();
nestedContentStackPane.getChildren().add(nestedContentPane);
nestedContentAnchorPane = new AnchorPane();
nestedContentAnchorPane.setStyle("-fx-background-color: white;");
nestedContentAnchorPane.getChildren().add(nestedContentStackPane);
AnchorPane.setLeftAnchor(nestedContentStackPane, -0.5d);
AnchorPane.setTopAnchor(nestedContentStackPane, -0.5d);
AnchorPane.setRightAnchor(nestedContentStackPane, 0.5d);
AnchorPane.setBottomAnchor(nestedContentStackPane, 0.5d);
VBox.setVgrow(nestedContentAnchorPane, Priority.ALWAYS);
// put nested content stack pane below image and text
vbox = new VBox();
vbox.setMouseTransparent(true);
vbox.setAlignment(Pos.CENTER);
vbox.getChildren().addAll(hbox);
vbox.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
// place the box below the other visuals
group.getChildren().addAll(shape, vbox);
return group;
}
@Override
protected void doDeactivate() {
getContent().attributesProperty().removeListener(nodeAttributesObserver);
super.doDeactivate();
}
@Override
protected SetMultimap<? extends Object, String> doGetContentAnchorages() {
return HashMultimap.create();
}
@Override
protected List<? extends Object> doGetContentChildren() {
Graph nestedGraph = getContent().getNestedGraph();
if (nestedGraph == null) {
return Collections.emptyList();
}
// only show children when zoomed in
Transform tx = getVisual().getLocalToSceneTransform();
double scale = FX2Geometry.toAffineTransform(tx).getScaleX();
if (scale > ZOOMLEVEL_SHOW_NESTED_GRAPH) {
return Collections.singletonList(nestedGraph);
}
return Collections.emptyList();
}
@Override
protected void doRefreshVisual(Group visual) {
org.eclipse.gef.graph.Node node = getContent();
if (node == null) {
throw new IllegalStateException();
}
// set CSS class
Map<String, Object> attrs = node.attributesProperty();
List<String> cssClasses = new ArrayList<>();
cssClasses.add(CSS_CLASS);
if (attrs.containsKey(ZestProperties.CSS_CLASS__NE)) {
cssClasses.add(ZestProperties.getCssClass(node));
}
if (!visual.getStyleClass().equals(cssClasses)) {
visual.getStyleClass().setAll(cssClasses);
}
// set CSS id
String id = null;
if (attrs.containsKey(ZestProperties.CSS_ID__NE)) {
id = ZestProperties.getCssId(node);
}
if (visual.getId() != id || id != null && !id.equals(visual.getId())) {
visual.setId(id);
}
refreshShape();
// set CSS style
if (attrs.containsKey(ZestProperties.SHAPE_CSS_STYLE__N)) {
if (getShape() != null) {
if (!getShape().getStyle().equals(ZestProperties.getShapeCssStyle(node))) {
getShape().setStyle(ZestProperties.getShapeCssStyle(node));
}
}
}
if (attrs.containsKey(ZestProperties.LABEL_CSS_STYLE__NE)) {
if (getLabelText() != null) {
if (!getLabelText().getStyle().equals(ZestProperties.getLabelCssStyle(node))) {
getLabelText().setStyle(ZestProperties.getLabelCssStyle(node));
}
}
}
if (vbox != null) {
if (getShape() != null && DEFAULT_SHAPE_ROLE.equals(getShape().getUserData()) || isNesting()) {
vbox.setPadding(new Insets(DEFAULT_SHAPE_PADDING));
} else {
vbox.setPadding(Insets.EMPTY);
}
if (isNesting()) {
if (!vbox.getChildren().contains(nestedContentAnchorPane)) {
vbox.getChildren().add(nestedContentAnchorPane);
if (vbox.getPrefWidth() == Region.USE_COMPUTED_SIZE
&& vbox.getPrefHeight() == Region.USE_COMPUTED_SIZE) {
vbox.setPrefSize(DEFAULT_OUTER_LAYOUT_CONTAINER_WIDTH_NESTING,
DEFAULT_OUTER_LAYOUT_CONTAINER_HEIGHT_NESTING);
vbox.autosize();
}
}
// show a nested graph icon dependent on the zoom level
if (!getChildrenUnmodifiable().isEmpty()) {
hideNestedGraphIcon();
} else {
// show an icon as a replacement when the zoom threshold is
// not reached
showNestedGraphIcon();
}
} else {
if (vbox.getChildren().contains(nestedContentAnchorPane)) {
vbox.getChildren().remove(nestedContentAnchorPane);
vbox.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
vbox.autosize();
}
}
}
refreshLabel();
refreshIcon();
refreshTooltip();
Point position = ZestProperties.getPosition(node);
if (position != null) {
Affine newTransform = new Affine(new Translate(position.x, position.y));
if (!NodeUtils.equals(getVisualTransform(), newTransform)) {
setVisualTransform(newTransform);
}
}
Dimension size = ZestProperties.getSize(node);
if (size != null) {
// XXX: Resize is needed even though the visual size is already
// up-to-date, because otherwise a nesting node might be resized to
// 0, 0 (unknown reason, need debug).
getVisual().resize(size.width, size.height);
} else {
getVisual().autosize();
}
}
@Override
protected void doRemoveChildVisual(IVisualPart<? extends Node> child, int index) {
getNestedContentPane().getChildren().remove(index);
}
@Override
public org.eclipse.gef.graph.Node getContent() {
return (org.eclipse.gef.graph.Node) super.getContent();
}
@Override
public Dimension getContentSize() {
Dimension size = ZestProperties.getSize(getContent());
if (size == null) {
size = new Dimension();
}
return size;
}
@Override
public Affine getContentTransform() {
Point position = ZestProperties.getPosition(getContent());
if (position == null) {
position = new Point();
}
return new Affine(new Translate(position.x, position.y));
}
/**
* Returns the {@link ImageView} that displays the node's icon.
*
* @return The {@link ImageView} that displays the node's icon.
*/
protected ImageView getIconImageView() {
return iconImageView;
}
/**
* Returns the {@link Text} that displays the node's label.
*
* @return The {@link Text} that displays the node's label.
*/
protected Text getLabelText() {
return labelText;
}
/**
* Returns the {@link Pane} to which nested children are added.
*
* @return The {@link Pane} to which nested children are added.
*/
private Pane getNestedContentPane() {
return nestedContentPane;
}
/**
* Returns the {@link StackPane} that either displays nested content or an
* icon indicating that nested content exists for this {@link NodePart}.
*
* @return The {@link StackPane} that wraps nested content.
*/
private StackPane getNestedContentStackPane() {
return nestedContentStackPane;
}
/**
* Returns the {@link Shape} that displays the node's border and background.
*
* @return The {@link Shape} that displays the node's border and background.
*/
public Node getShape() {
return shape;
}
/**
* Removes the {@link #getNestedGraphIcon()} from the
* {@link #getNestedContentStackPane()} and {@link #setNestedGraphIcon(Node)
* sets} the nested graph icon to <code>null</code>.
*/
private void hideNestedGraphIcon() {
if (nestedGraphIcon != null) {
getNestedContentStackPane().getChildren().remove(nestedGraphIcon);
nestedGraphIcon = null;
}
}
/**
* Returns <code>true</code> if this {@link NodePart} contains a nested
* {@link Graph}. Otherwise, <code>false</code> is returned.
*
* @return <code>true</code> if this {@link NodePart} contains a nested
* {@link Graph}, otherwise <code>false</code>.
*/
private boolean isNesting() {
return getContent().getNestedGraph() != null;
}
/**
* If the given <i>icon</i> is an {@link Image}, that {@link Image} will be
* used as the icon of this {@link NodePart}.
*/
protected void refreshIcon() {
Image icon = ZestProperties.getIcon(getContent());
if (getIconImageView() != null && getIconImageView().getImage() != icon) {
getIconImageView().setImage(icon);
}
}
/**
* Changes the label of this {@link NodePart} to the given value.
*/
protected void refreshLabel() {
String label = ZestProperties.getLabel(getContent());
if (label == null || label.isEmpty()) {
label = NODE_LABEL_EMPTY;
}
if (getLabelText() != null && !getLabelText().getText().equals(label)) {
getLabelText().setText(label);
}
}
private void refreshShape() {
Node shape = ZestProperties.getShape(getContent());
if (this.shape != shape && shape != null) {
getVisual().getChildren().remove(shape);
this.shape = shape;
if (shape instanceof GeometryNode) {
((GeometryNode<?>) shape).setStrokeType(StrokeType.INSIDE);
} else if (shape instanceof Shape) {
((Shape) shape).setStrokeType(StrokeType.INSIDE);
}
if (!shape.getStyleClass().contains(CSS_CLASS_SHAPE)) {
shape.getStyleClass().add(CSS_CLASS_SHAPE);
}
getVisual().getChildren().add(0, shape);
}
}
/**
* Changes the tooltip of this {@link NodePart} to the given value.
*
*/
protected void refreshTooltip() {
String tooltip = ZestProperties.getTooltip(getContent());
if (tooltip != null && !tooltip.isEmpty()) {
if (tooltipNode == null) {
tooltipNode = new Tooltip(tooltip);
Tooltip.install(getVisual(), tooltipNode);
} else {
tooltipNode.setText(tooltip);
}
} else {
if (tooltipNode != null) {
Tooltip.uninstall(getVisual(), tooltipNode);
}
}
}
@Override
public void setContentSize(Dimension size) {
ZestProperties.setSize(getContent(), size);
}
@Override
public void setContentTransform(Affine totalTransform) {
ZestProperties.setPosition(getContent(), new Point(totalTransform.getTx(), totalTransform.getTy()));
}
/**
* Creates the nested graph icon and adds it to the
* {@link #getNestedContentStackPane()}.
*/
private void showNestedGraphIcon() {
if (nestedGraphIcon == null) {
nestedGraphIcon = new NestedGraphIcon();
getNestedContentStackPane().getChildren().add(nestedGraphIcon);
}
}
}