/* * Copyright 2014 Red Hat, Inc. and/or its affiliates. * * 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.drools.workbench.screens.guided.dtree.client.widget; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; import javax.inject.Inject; 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.shape.Group; import com.ait.lienzo.client.core.shape.Rectangle; import com.ait.lienzo.client.core.shape.Text; import com.ait.lienzo.client.core.types.Point2D; import com.ait.lienzo.shared.core.types.TextAlign; import com.ait.lienzo.shared.core.types.TextBaseLine; import com.google.gwt.user.client.Window; import org.drools.workbench.models.guided.dtree.shared.model.GuidedDecisionTree; import org.drools.workbench.models.guided.dtree.shared.model.nodes.ActionInsertNode; import org.drools.workbench.models.guided.dtree.shared.model.nodes.ActionRetractNode; import org.drools.workbench.models.guided.dtree.shared.model.nodes.ActionUpdateNode; import org.drools.workbench.models.guided.dtree.shared.model.nodes.ConstraintNode; import org.drools.workbench.models.guided.dtree.shared.model.nodes.Node; import org.drools.workbench.models.guided.dtree.shared.model.nodes.TypeNode; import org.drools.workbench.screens.guided.dtree.client.editor.GuidedDecisionTreeEditorPresenter; import org.drools.workbench.screens.guided.dtree.client.resources.i18n.GuidedDecisionTreeConstants; import org.drools.workbench.screens.guided.dtree.client.widget.factories.ActionInsertNodeFactory; import org.drools.workbench.screens.guided.dtree.client.widget.factories.ActionRetractNodeFactory; import org.drools.workbench.screens.guided.dtree.client.widget.factories.ActionUpdateNodeFactory; import org.drools.workbench.screens.guided.dtree.client.widget.factories.ConstraintNodeFactory; import org.drools.workbench.screens.guided.dtree.client.widget.factories.TypeNodeFactory; import org.drools.workbench.screens.guided.dtree.client.widget.shapes.BaseGuidedDecisionTreeShape; import org.drools.workbench.screens.guided.dtree.client.widget.shapes.TypeShape; import org.uberfire.client.mvp.UberView; import org.uberfire.commons.data.Pair; import org.uberfire.ext.wires.core.api.events.ClearEvent; import org.uberfire.ext.wires.core.api.events.ShapeAddedEvent; import org.uberfire.ext.wires.core.api.events.ShapeDeletedEvent; import org.uberfire.ext.wires.core.api.events.ShapeDragCompleteEvent; import org.uberfire.ext.wires.core.api.events.ShapeDragPreviewEvent; import org.uberfire.ext.wires.core.api.events.ShapeSelectedEvent; 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.WiresBaseShape; import org.uberfire.ext.wires.core.client.canvas.WiresCanvas; import org.uberfire.ext.wires.core.client.util.ShapeFactoryUtil; 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.ext.wires.core.trees.client.shapes.WiresBaseTreeNode; public class GuidedDecisionTreeWidget extends WiresCanvas implements UberView<GuidedDecisionTreeEditorPresenter> { private static final int MAX_PROXIMITY = 200; private static final int ANIMATION_DURATION = 250; @Inject private Event<ClearEvent> clearEvent; @Inject private Event<ShapeSelectedEvent> shapeSelectedEvent; @Inject private Event<ShapeAddedEvent> shapeAddedEvent; @Inject private Event<ShapeDeletedEvent> shapeDeletedEvent; @Inject private LayoutManager layoutManager; @Inject private TypeNodeFactory typeNodeFactory; @Inject private ConstraintNodeFactory constraintNodeFactory; @Inject private ActionInsertNodeFactory actionInsertNodeFactory; @Inject private ActionUpdateNodeFactory actionUpdateNodeFactory; @Inject private ActionRetractNodeFactory actionRetractNodeFactory; private GuidedDecisionTreeDropContext dropContext = new GuidedDecisionTreeDropContext(); private WiresTreeNodeConnector connector = null; private WiresBaseTreeNode uiRoot; private GuidedDecisionTree model; private GuidedDecisionTreeEditorPresenter presenter; private Group hint = null; private boolean isGettingStartedHintVisible = false; @Override public void init( final GuidedDecisionTreeEditorPresenter presenter ) { this.presenter = presenter; } @Override public void selectShape( final WiresBaseShape shape ) { shapeSelectedEvent.fire( new ShapeSelectedEvent( shape ) ); } public void onShapeSelected( @Observes ShapeSelectedEvent event ) { final WiresBaseShape shape = event.getShape(); super.selectShape( shape ); } @Override public void deselectShape( final WiresBaseShape shape ) { super.deselectShape( shape ); } public void onDragPreviewHandler( @Observes ShapeDragPreviewEvent shapeDragPreviewEvent ) { //We can only connect WiresTreeNodes to each other if ( !( shapeDragPreviewEvent.getShape() instanceof BaseGuidedDecisionTreeShape ) ) { dropContext.setContext( null ); return; } //Find a Parent Node to attach the Shape to final double cx = getX( shapeDragPreviewEvent.getX() ); final double cy = getY( shapeDragPreviewEvent.getY() ); final BaseGuidedDecisionTreeShape uiChild = (BaseGuidedDecisionTreeShape) shapeDragPreviewEvent.getShape(); final BaseGuidedDecisionTreeShape uiProspectiveParent = getParentNode( uiChild, cx, cy ); //If there is a prospective parent show the line between child and parent if ( uiProspectiveParent != null ) { if ( connector == null ) { connector = new WiresTreeNodeConnector(); canvasLayer.add( connector ); connector.moveToBottom(); } connector.getPoints().get( 0 ).set( uiProspectiveParent.getLocation() ); connector.getPoints().get( 1 ).set( new Point2D( cx, cy ) ); } else if ( connector != null ) { canvasLayer.remove( connector ); connector = null; } dropContext.setContext( uiProspectiveParent ); canvasLayer.batch(); } public void onDragCompleteHandler( @Observes ShapeDragCompleteEvent shapeDragCompleteEvent ) { final WiresBaseShape wiresShape = shapeDragCompleteEvent.getShape(); //Hide the temporary connector if ( connector != null ) { canvasLayer.remove( connector ); canvasLayer.batch(); connector = null; } //If there's no Shape to add then exit if ( wiresShape == null ) { dropContext.setContext( null ); return; } //If the Shape is not intended for the Guided Decision Tree widget then exit if ( !( wiresShape instanceof BaseGuidedDecisionTreeShape ) ) { dropContext.setContext( null ); return; } final BaseGuidedDecisionTreeShape uiChild = (BaseGuidedDecisionTreeShape) wiresShape; //Get Shape's co-ordinates relative to the Canvas final double cx = getX( shapeDragCompleteEvent.getX() ); final double cy = getY( shapeDragCompleteEvent.getY() ); //If the Shape was dropped outside the bounds of the Canvas then exit if ( cx < 0 || cy < 0 ) { dropContext.setContext( null ); return; } final int scrollWidth = getElement().getScrollWidth(); final int scrollHeight = getElement().getScrollHeight(); if ( cx > scrollWidth || cy > scrollHeight ) { dropContext.setContext( null ); return; } //Add the new Node to it's parent (unless this is the first node) final BaseGuidedDecisionTreeShape uiParent = dropContext.getContext(); boolean addShape = ( ( getShapesInCanvas().size() == 0 && ( uiChild instanceof TypeShape ) ) || ( getShapesInCanvas().size() > 0 && uiParent != null ) ); boolean addChildToParent = uiParent != null; if ( addShape ) { uiChild.setX( cx ); uiChild.setY( cy ); if ( addChildToParent ) { uiParent.addChildNode( uiChild ); uiParent.getModelNode().addChild( uiChild.getModelNode() ); } else if ( uiChild instanceof TypeShape ) { uiRoot = uiChild; model.setRoot( ( (TypeShape) uiChild ).getModelNode() ); } addShape( uiChild ); //Notify other Panels of a Shape being added shapeAddedEvent.fire( new ShapeAddedEvent( uiChild ) ); } } private double getX( double xShapeEvent ) { return xShapeEvent - getAbsoluteLeft(); } private double getY( double yShapeEvent ) { return yShapeEvent - getAbsoluteTop(); } @Override public void clear() { if ( Window.confirm( GuidedDecisionTreeConstants.INSTANCE.confirmDeleteDecisionTree() ) ) { super.clear(); clearEvent.fire( new ClearEvent() ); uiRoot = null; } } @Override public void deleteShape( final WiresBaseShape shape ) { if ( Window.confirm( GuidedDecisionTreeConstants.INSTANCE.confirmDeleteDecisionTreeNode() ) ) { if ( uiRoot != null && uiRoot.equals( shape ) ) { uiRoot = null; model.setRoot( null ); } else if ( shape instanceof BaseGuidedDecisionTreeShape ) { final BaseGuidedDecisionTreeShape uiChild = (BaseGuidedDecisionTreeShape) shape; if ( uiChild.getParentNode() instanceof BaseGuidedDecisionTreeShape ) { final BaseGuidedDecisionTreeShape uiParent = (BaseGuidedDecisionTreeShape) uiChild.getParentNode(); uiParent.getModelNode().removeChild( uiChild.getModelNode() ); } } shapeDeletedEvent.fire( new ShapeDeletedEvent( shape ) ); layout(); } } @Override public void forceDeleteShape( final WiresBaseShape shape ) { shapeDeletedEvent.fire( new ShapeDeletedEvent( shape ) ); } public void onShapeDeleted( @Observes ShapeDeletedEvent event ) { super.deleteShape( event.getShape() ); if ( getShapesInCanvas().isEmpty() ) { showGettingStartedHint(); } } @Override public void addShape( final WiresBaseShape shape ) { super.addShape( shape ); //Attach relevant handlers if ( shape instanceof RequiresLayoutManager ) { ( (RequiresLayoutManager) shape ).setLayoutManager( layoutManager ); } if ( shape instanceof BaseGuidedDecisionTreeShape ) { ( (BaseGuidedDecisionTreeShape) shape ).setPresenter( presenter ); } if ( !getShapesInCanvas().isEmpty() ) { hideGettingStartedHint(); } layout(); } public void setModel( final GuidedDecisionTree model, final boolean isReadOnly ) { this.uiRoot = null; this.model = model; //Clear existing state super.clear(); clearEvent.fire( new ClearEvent() ); //Walk model creating UIModel final TypeNode root = model.getRoot(); if ( root != null ) { final WiresBaseTreeNode uiRoot = typeNodeFactory.getShape( root, isReadOnly ); this.uiRoot = uiRoot; processChildren( root, uiRoot, isReadOnly ); final Map<WiresBaseShape, Point2D> layout = layoutManager.getLayoutInformation( uiRoot ); final Rectangle2D canvasBounds = WiresLayoutUtilities.alignLayoutInCanvas( layout ); for ( Map.Entry<WiresBaseShape, Point2D> e : layout.entrySet() ) { final Point2D destination = new Point2D( e.getValue().getX(), e.getValue().getY() ); e.getKey().setLocation( destination ); } WiresLayoutUtilities.resizeViewPort( canvasBounds, canvasLayer.getViewport() ); } if ( shapesInCanvas.isEmpty() ) { showGettingStartedHint(); } canvasLayer.batch(); } private void processChildren( final Node node, final WiresBaseTreeNode uiNode, final boolean isReadOnly ) { uiNode.setSelectionManager( this ); uiNode.setShapesManager( this ); uiNode.setLayoutManager( layoutManager ); if ( uiNode instanceof BaseGuidedDecisionTreeShape ) { ( (BaseGuidedDecisionTreeShape) uiNode ).setPresenter( presenter ); } canvasLayer.add( uiNode ); shapesInCanvas.add( uiNode ); final Iterator<Node> itr = node.iterator(); while ( itr.hasNext() ) { final Node child = itr.next(); WiresBaseTreeNode uiChildNode = null; if ( child instanceof TypeNode ) { uiChildNode = typeNodeFactory.getShape( (TypeNode) child, isReadOnly ); } else if ( child instanceof ConstraintNode ) { uiChildNode = constraintNodeFactory.getShape( (ConstraintNode) child, isReadOnly ); } else if ( child instanceof ActionInsertNode ) { uiChildNode = actionInsertNodeFactory.getShape( (ActionInsertNode) child, isReadOnly ); } else if ( child instanceof ActionUpdateNode ) { uiChildNode = actionUpdateNodeFactory.getShape( (ActionUpdateNode) child, isReadOnly ); } else if ( child instanceof ActionRetractNode ) { uiChildNode = actionRetractNodeFactory.getShape( (ActionRetractNode) child, isReadOnly ); } if ( uiChildNode != null ) { uiNode.addChildNode( uiChildNode ); processChildren( child, uiChildNode, isReadOnly ); } } } protected BaseGuidedDecisionTreeShape getParentNode( final BaseGuidedDecisionTreeShape uiChild, final double cx, final double cy ) { BaseGuidedDecisionTreeShape uiProspectiveParent = null; double finalDistance = Double.MAX_VALUE; for ( WiresBaseShape ws : getShapesInCanvas() ) { if ( ws.isVisible() ) { if ( ws instanceof BaseGuidedDecisionTreeShape ) { final BaseGuidedDecisionTreeShape uiNode = (BaseGuidedDecisionTreeShape) ws; if ( uiNode.acceptChildNode( uiChild ) && !uiNode.hasCollapsedChildren() ) { double deltaX = cx - uiNode.getX(); double deltaY = cy - uiNode.getY(); double distance = Math.sqrt( Math.pow( deltaX, 2 ) + Math.pow( deltaY, 2 ) ); if ( finalDistance > distance ) { finalDistance = distance; uiProspectiveParent = uiNode; } } } } } //If we're too far away from a parent we might as well not have a parent if ( finalDistance > MAX_PROXIMITY ) { uiProspectiveParent = null; } return uiProspectiveParent; } private void layout() { //Get layout information final Map<WiresBaseShape, Point2D> layout = layoutManager.getLayoutInformation( uiRoot ); final Rectangle2D canvasBounds = WiresLayoutUtilities.alignLayoutInCanvas( layout ); //Run an animation to move WiresBaseTreeNodes from their current position to the target position uiRoot.animate( AnimationTweener.EASE_OUT, new AnimationProperties(), ANIMATION_DURATION, new IAnimationCallback() { private final Map<WiresBaseShape, Pair<Point2D, Point2D>> transformations = new HashMap<WiresBaseShape, Pair<Point2D, Point2D>>(); @Override public void onStart( final IAnimation iAnimation, final IAnimationHandle iAnimationHandle ) { //Reposition nodes. First we store the WiresBaseTreeNode together with its current position and target position transformations.clear(); for ( Map.Entry<WiresBaseShape, Point2D> e : layout.entrySet() ) { final Point2D origin = e.getKey().getLocation(); final Point2D destination = new Point2D( e.getValue().getX(), e.getValue().getY() ); transformations.put( e.getKey(), new Pair<Point2D, Point2D>( origin, destination ) ); } WiresLayoutUtilities.resizeViewPort( canvasBounds, canvasLayer.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 ); } //Without this call Lienzo doesn't update the Canvas for sub-classes of WiresBaseTreeNode uiRoot.getLayer().batch(); } @Override public void onClose( final IAnimation iAnimation, final IAnimationHandle iAnimationHandle ) { //Nothing to do } } ); canvasLayer.batch(); } private void showGettingStartedHint() { if ( isGettingStartedHintVisible ) { return; } if ( hint == null ) { hint = new Group(); final Rectangle hintRectangle = new Rectangle( 600, 225, 15 ); hintRectangle.setStrokeWidth( 2.0 ); hintRectangle.setStrokeColor( "#6495ED" ); hintRectangle.setFillColor( "#AFEEEE" ); hintRectangle.setAlpha( 0.75 ); final Text hintText = new Text( GuidedDecisionTreeConstants.INSTANCE.gettingStartedHint(), ShapeFactoryUtil.FONT_FAMILY_DESCRIPTION, 18 ); hintText.setTextAlign( TextAlign.CENTER ); hintText.setTextBaseLine( TextBaseLine.MIDDLE ); hintText.setFillColor( "#6495ED" ); hintText.setX( hintRectangle.getWidth() / 2 ); hintText.setY( hintRectangle.getHeight() / 2 ); hint.setX( ( canvasLayer.getWidth() - hintRectangle.getWidth() ) / 2 ); hint.setY( ( canvasLayer.getHeight() / 3 ) - ( hintRectangle.getHeight() / 2 ) ); hint.add( hintRectangle ); hint.add( hintText ); } hint.animate( AnimationTweener.LINEAR, new AnimationProperties(), ANIMATION_DURATION, new IAnimationCallback() { @Override public void onStart( final IAnimation iAnimation, final IAnimationHandle iAnimationHandle ) { hint.setAlpha( 0.0 ); canvasLayer.add( hint ); isGettingStartedHintVisible = true; } @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(); hint.setAlpha( pct ); hint.getLayer().batch(); } @Override public void onClose( final IAnimation iAnimation, final IAnimationHandle iAnimationHandle ) { //Nothing to do } } ); } private void hideGettingStartedHint() { if ( !isGettingStartedHintVisible ) { return; } hint.animate( AnimationTweener.LINEAR, new AnimationProperties(), ANIMATION_DURATION, new IAnimationCallback() { @Override public void onStart( final IAnimation iAnimation, final IAnimationHandle iAnimationHandle ) { //Nothing to do } @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(); hint.setAlpha( 1 - pct ); hint.getLayer().batch(); } @Override public void onClose( final IAnimation iAnimation, final IAnimationHandle iAnimationHandle ) { canvasLayer.remove( hint ); isGettingStartedHintVisible = false; } } ); } }