/* * Copyright 2015 JBoss, by Red Hat, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.uberfire.ext.wires.core.trees.client.shapes; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import com.ait.lienzo.client.core.animation.AnimationProperties; import com.ait.lienzo.client.core.animation.AnimationTweener; import com.ait.lienzo.client.core.animation.IAnimation; import com.ait.lienzo.client.core.animation.IAnimationCallback; import com.ait.lienzo.client.core.animation.IAnimationHandle; import com.ait.lienzo.client.core.event.NodeDragMoveEvent; import com.ait.lienzo.client.core.event.NodeDragMoveHandler; import com.ait.lienzo.client.core.shape.Group; import com.ait.lienzo.client.core.types.Point2D; import org.uberfire.commons.data.Pair; import org.uberfire.ext.wires.core.api.layout.LayoutManager; import org.uberfire.ext.wires.core.api.layout.RequiresLayoutManager; import org.uberfire.ext.wires.core.api.shapes.RequiresShapesManager; import org.uberfire.ext.wires.core.api.shapes.ShapesManager; import org.uberfire.ext.wires.core.api.shapes.WiresBaseShape; import org.uberfire.ext.wires.core.trees.client.canvas.WiresTreeNodeConnector; import org.uberfire.ext.wires.core.trees.client.layout.WiresLayoutUtilities; import org.uberfire.ext.wires.core.trees.client.layout.treelayout.Rectangle2D; import org.uberfire.mvp.Command; public abstract class WiresBaseTreeNode extends WiresBaseShape implements RequiresShapesManager, RequiresLayoutManager { private static final int ANIMATION_DURATION = 250; protected ShapesManager shapesManager; protected LayoutManager layoutManager; private WiresBaseTreeNode parent; private List<WiresBaseTreeNode> children = new ArrayList<WiresBaseTreeNode>(); private List<WiresTreeNodeConnector> connectors = new ArrayList<WiresTreeNodeConnector>(); private IAnimationHandle animationHandle; private int collapsed = 0; public WiresBaseTreeNode() { //Update connectors when this Node moves addNodeDragMoveHandler(new NodeDragMoveHandler() { @Override public void onNodeDragMove(final NodeDragMoveEvent nodeDragMoveEvent) { for (WiresTreeNodeConnector connector : connectors) { connector.getPoints().get(0).set(getLocation()); } getLayer().batch(); } }); } @Override public void setShapesManager(final ShapesManager shapesManager) { this.shapesManager = shapesManager; } @Override public void setLayoutManager(final LayoutManager layoutManager) { this.layoutManager = layoutManager; } @Override public boolean contains(final double cx, final double cy) { return false; } @Override public void destroy() { //Remove children final List<WiresBaseTreeNode> cloneChildren = new ArrayList<WiresBaseTreeNode>(children); for (WiresBaseTreeNode child : cloneChildren) { shapesManager.forceDeleteShape(child); } children.clear(); //Remove connectors to children final List<WiresTreeNodeConnector> cloneConnectors = new ArrayList<WiresTreeNodeConnector>(connectors); for (WiresTreeNodeConnector connector : cloneConnectors) { getLayer().remove(connector); } connectors.clear(); //Remove from its parent if (parent != null) { parent.removeChildNode(this); } super.destroy(); } public WiresBaseTreeNode getParentNode() { return this.parent; } public void setParentNode(final WiresBaseTreeNode parent) { this.parent = parent; } /** * TreeNodes can decide to accept child TreeNodes when being dragged from the Palette to a prospective parent * @param child TreeNode that will be added to this TreeNode as a child * @return true if the child can be added to this TreeNode */ public boolean acceptChildNode(final WiresBaseTreeNode child) { //Accept all types of WiresBaseTreeNode by default return true; } /** * Add a TreeNode as a child to this TreeNode. A connector is automatically created and maintained for the child. * Connectors are "outgoing" from the parent to a child. * @param child */ public void addChildNode(final WiresBaseTreeNode child) { final WiresTreeNodeConnector connector = new WiresTreeNodeConnector(); connector.getPoints().get(0).set(getLocation()); connector.getPoints().get(1).set(child.getLocation()); getLayer().add(connector); connector.moveToBottom(); final int index = getChildIndex(connector); children.add(index, child); connectors.add(index, connector); child.setParentNode(this); //Update connectors when child Node moves child.addNodeDragMoveHandler(new NodeDragMoveHandler() { @Override public void onNodeDragMove(final NodeDragMoveEvent nodeDragMoveEvent) { connector.getPoints().get(1).set(child.getLocation()); } }); } //Get the index of the new child connector by determining the angle of existing connectors it lays in between private int getChildIndex(final WiresTreeNodeConnector newConnector) { final double newConnectorTheta = getConnectorAngle(newConnector); for (int index = 0; index < connectors.size(); index++) { final WiresTreeNodeConnector existingConnector = connectors.get(index); final double existingConnectorTheta = getConnectorAngle(existingConnector); if (newConnectorTheta > existingConnectorTheta) { return index; } } return connectors.size(); } private double getConnectorAngle(final WiresTreeNodeConnector connector) { final double cdx = connector.getPoints().get(1).getX() - connector.getPoints().get(0).getX(); final double cdy = connector.getPoints().get(1).getY() - connector.getPoints().get(0).getY(); final double theta = Math.atan2(cdy, cdx) + Math.PI / 2; return (theta < 0 ? theta + (2 * Math.PI) : theta); } /** * Remove a child TreeNode from this TreeNode. Connectors are automatically cleared up. * @param child */ public void removeChildNode(final WiresBaseTreeNode child) { child.setParentNode(null); final int index = children.indexOf(child); final WiresTreeNodeConnector connector = connectors.get(index); children.remove(child); connectors.remove(connector); getLayer().remove(connector); } public List<WiresBaseTreeNode> getChildren() { return this.children; } public abstract double getWidth(); public abstract double getHeight(); private void childMoved(final WiresBaseTreeNode child, final double nx, final double ny) { final int index = children.indexOf(child); final WiresTreeNodeConnector connector = connectors.get(index); connector.getPoints().get(1).setX(nx); connector.getPoints().get(1).setY(ny); } /** * Collapse this TreeNode and all descendants. * @param callback The callback is invoked when the animation completes. */ public void collapse(final Command callback) { //This TreeNode is already collapsed if (!hasChildren() || hasCollapsedChildren()) { return; } if (animationHandle != null) { animationHandle.stop(); } animationHandle = animate(AnimationTweener.EASE_OUT, new AnimationProperties(), ANIMATION_DURATION, new IAnimationCallback() { private List<WiresBaseTreeNode> descendants; private Map<WiresBaseShape, Pair<Point2D, Point2D>> transformations = new HashMap<WiresBaseShape, Pair<Point2D, Point2D>>(); private Map<WiresBaseShape, Point2D> layout; private Rectangle2D canvasBounds; @Override public void onStart(final IAnimation iAnimation, final IAnimationHandle iAnimationHandle) { //Mark all descendants as collapsed, which affects the layout information descendants = getDescendants(WiresBaseTreeNode.this); for (WiresBaseTreeNode descendant : descendants) { descendant.collapsed++; } //Get new layout information layout = layoutManager.getLayoutInformation(getTreeRoot()); canvasBounds = WiresLayoutUtilities.alignLayoutInCanvas(layout); //Store required transformations: Shape, Current location, Target location transformations.clear(); for (Map.Entry<WiresBaseShape, Point2D> e : layout.entrySet()) { final Point2D origin = e.getKey().getLocation(); final Point2D destination = e.getValue(); transformations.put(e.getKey(), new Pair<Point2D, Point2D>(origin, destination)); } //Allow subclasses to change their appearance onCollapseStart(); } @Override public void onFrame(final IAnimation iAnimation, final IAnimationHandle iAnimationHandle) { //Lienzo's IAnimation.getPercent() passes values > 1.0 final double pct = iAnimation.getPercent() > 1.0 ? 1.0 : iAnimation.getPercent(); //Move each descendant along the line between its origin and the target destination for (Map.Entry<WiresBaseShape, Pair<Point2D, Point2D>> e : transformations.entrySet()) { final Point2D descendantOrigin = e.getValue().getK1(); final Point2D descendantTarget = e.getValue().getK2(); final double dx = (descendantTarget.getX() - descendantOrigin.getX()) * pct; final double dy = (descendantTarget.getY() - descendantOrigin.getY()) * pct; e.getKey().setX(descendantOrigin.getX() + dx); e.getKey().setY(descendantOrigin.getY() + dy); } for (WiresBaseTreeNode descendant : descendants) { descendant.setAlpha(1.0 - pct); } //Allow subclasses to change their appearance onCollapseProgress(pct); //Without this call Lienzo doesn't update the Canvas for sub-classes of WiresBaseTreeNode WiresBaseTreeNode.this.getLayer().batch(); } @Override public void onClose(final IAnimation iAnimation, final IAnimationHandle iAnimationHandle) { //Hide connectors, descendants and descendants' connectors when complete for (WiresTreeNodeConnector connector : connectors) { connector.setVisible(false); } for (WiresBaseTreeNode descendant : descendants) { descendant.setVisible(false); for (WiresTreeNodeConnector connector : descendant.connectors) { connector.setVisible(false); } } //Allow subclasses to change their appearance onCollapseEnd(); //Invoke callback if one was provided if (callback != null) { callback.execute(); } WiresLayoutUtilities.resizeViewPort(canvasBounds, WiresBaseTreeNode.this.getViewport()); } }); getLayer().batch(); } /** * Called when the TreeNode is about to be collapsed. Default implementation does nothing. */ public void onCollapseStart() { //Do nothing by default } /** * Called while the TreeNode is being collapsed. Default implementation does nothing. * @param pct 0.0 to 1.0 where 1.0 is collapsed */ public void onCollapseProgress(final double pct) { //Do nothing by default } /** * Called when the TreeNode has been collapsed. Default implementation does nothing. */ public void onCollapseEnd() { //Do nothing by default } /** * Expand this TreeNode and all descendants. Nested collapsed TreeNodes are not expanded. * @param callback The callback is invoked when the animation completes. */ public void expand(final Command callback) { //This TreeNode is already expanded if (!hasCollapsedChildren()) { return; } if (animationHandle != null) { animationHandle.stop(); } animationHandle = animate(AnimationTweener.EASE_OUT, new AnimationProperties(), ANIMATION_DURATION, new IAnimationCallback() { private List<WiresBaseTreeNode> descendants; private Map<WiresBaseShape, Pair<Point2D, Point2D>> transformations = new HashMap<WiresBaseShape, Pair<Point2D, Point2D>>(); @Override public void onStart(final IAnimation iAnimation, final IAnimationHandle iAnimationHandle) { //Show connectors to this node's immediate children for (WiresTreeNodeConnector connector : connectors) { connector.setVisible(true); } //Show child nodes and connectors if they are not still collapsed descendants = getDescendants(WiresBaseTreeNode.this); for (WiresBaseTreeNode descendant : descendants) { descendant.collapsed--; if (descendant.collapsed == 0) { descendant.setVisible(true); } } for (WiresBaseTreeNode descendant : descendants) { for (WiresTreeNodeConnector connector : descendant.connectors) { connector.setVisible(!descendant.hasCollapsedChildren()); } } //Get new layout information final Map<WiresBaseShape, Point2D> layout = layoutManager.getLayoutInformation(getTreeRoot()); final Rectangle2D canvasBounds = WiresLayoutUtilities.alignLayoutInCanvas(layout); //Store required transformations: Shape, Current location, Target location transformations.clear(); for (Map.Entry<WiresBaseShape, Point2D> e : layout.entrySet()) { final Point2D origin = e.getKey().getLocation(); final Point2D destination = e.getValue(); transformations.put(e.getKey(), new Pair<Point2D, Point2D>(origin, destination)); } //Allow subclasses to change their appearance onExpandStart(); WiresLayoutUtilities.resizeViewPort(canvasBounds, WiresBaseTreeNode.this.getViewport()); } @Override public void onFrame(final IAnimation iAnimation, final IAnimationHandle iAnimationHandle) { //Lienzo's IAnimation.getPercent() passes values > 1.0 final double pct = iAnimation.getPercent() > 1.0 ? 1.0 : iAnimation.getPercent(); //Move each descendant along the line between its origin and the target destination for (Map.Entry<WiresBaseShape, Pair<Point2D, Point2D>> e : transformations.entrySet()) { final Point2D descendantOrigin = e.getValue().getK1(); final Point2D descendantTarget = e.getValue().getK2(); final double dx = (descendantTarget.getX() - descendantOrigin.getX()) * pct; final double dy = (descendantTarget.getY() - descendantOrigin.getY()) * pct; e.getKey().setX(descendantOrigin.getX() + dx); e.getKey().setY(descendantOrigin.getY() + dy); } for (WiresBaseTreeNode descendant : descendants) { descendant.setAlpha(pct); } //Allow subclasses to change their appearance onExpandProgress(pct); //Without this call Lienzo doesn't update the Canvas for sub-classes of WiresBaseTreeNode WiresBaseTreeNode.this.getLayer().batch(); } @Override public void onClose(final IAnimation iAnimation, final IAnimationHandle iAnimationHandle) { //Allow subclasses to change their appearance onExpandEnd(); //Invoke callback if one was provided if (callback != null) { callback.execute(); } } }); getLayer().batch(); } /** * Get the root node for the tree in which this node exists * @return The root */ private WiresBaseTreeNode getTreeRoot() { WiresBaseTreeNode root = this; while (root.parent != null) { root = root.parent; } return root; } /** * Called when the TreeNode is about to be expanded. Default implementation does nothing. */ public void onExpandStart() { //Do nothing by default } /** * Called while the TreeNode is being expanded. Default implementation does nothing. * @param pct 0.0 to 1.0 where 1.0 is expanded */ public void onExpandProgress(final double pct) { //Do nothing by default } /** * Called when the TreeNode has been expanded. Default implementation does nothing. */ public void onExpandEnd() { //Do nothing by default } protected List<WiresBaseTreeNode> getDescendants(final WiresBaseTreeNode node) { final List<WiresBaseTreeNode> descendants = new ArrayList<WiresBaseTreeNode>(); descendants.addAll(node.children); for (WiresBaseTreeNode child : node.children) { descendants.addAll(getDescendants(child)); } return descendants; } public boolean hasChildren() { return children.size() > 0; } public boolean hasCollapsedChildren() { for (WiresBaseTreeNode child : children) { if (child.collapsed > 0) { return true; } } return false; } //Move the Connector end-points to match where the descendant has been moved private void updateConnectorsEndPoints() { if (connectors == null) { return; } for (WiresTreeNodeConnector connector : connectors) { connector.getPoints().get(0).setX(getX()); connector.getPoints().get(0).setY(getY()); } if (parent != null) { parent.childMoved(this, getX(), getY()); } } @Override public Group setX(final double x) { final Group g = super.setX(x); updateConnectorsEndPoints(); return g; } @Override public Group setY(final double y) { final Group g = super.setY(y); updateConnectorsEndPoints(); return g; } }