/******************************************************************************
* Copyright (c) 2014 Oracle
* 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:
* Shenxue Zhou - initial implementation and ongoing maintenance
* Konstantin Komissarchik - [341856] NPE when a diagram connection doesn't define a label
* Konstantin Komissarchik - [342897] Integrate with properties view
* Konstantin Komissarchik - [342775] Support EL in MasterDetailsTreeNodeDef.ImagePath
* Konstantin Komissarchik - [378756] Convert ModelElementListener and ModelPropertyListener to common listener infrastructure
* Ling Hao - [383924] Flexible diagram node shapes
******************************************************************************/
package org.eclipse.sapphire.ui.diagram.internal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.sapphire.Element;
import org.eclipse.sapphire.ElementList;
import org.eclipse.sapphire.ElementType;
import org.eclipse.sapphire.Event;
import org.eclipse.sapphire.FilteredListener;
import org.eclipse.sapphire.Listener;
import org.eclipse.sapphire.Property;
import org.eclipse.sapphire.PropertyDef;
import org.eclipse.sapphire.PropertyEvent;
import org.eclipse.sapphire.ReferenceValue;
import org.eclipse.sapphire.Value;
import org.eclipse.sapphire.ValueProperty;
import org.eclipse.sapphire.modeling.ModelPath;
import org.eclipse.sapphire.modeling.ModelPath.ParentElementSegment;
import org.eclipse.sapphire.modeling.el.FunctionResult;
import org.eclipse.sapphire.services.ReferenceService;
import org.eclipse.sapphire.ui.Point;
import org.eclipse.sapphire.ui.SapphireActionSystem;
import org.eclipse.sapphire.ui.diagram.ConnectionBendpointsEvent;
import org.eclipse.sapphire.ui.diagram.ConnectionEndpointsEvent;
import org.eclipse.sapphire.ui.diagram.ConnectionLabelEvent;
import org.eclipse.sapphire.ui.diagram.DiagramConnectionPart;
import org.eclipse.sapphire.ui.diagram.def.IDiagramConnectionDef;
import org.eclipse.sapphire.ui.diagram.def.IDiagramExplicitConnectionBindingDef;
import org.eclipse.sapphire.ui.diagram.def.IDiagramLabelDef;
import org.eclipse.sapphire.ui.diagram.editor.DiagramNodePart;
import org.eclipse.sapphire.ui.diagram.editor.FunctionUtil;
import org.eclipse.sapphire.ui.diagram.editor.SapphireDiagramEditorPagePart;
import org.eclipse.sapphire.ui.diagram.internal.DiagramConnectionTemplate.ConnectionType;
import org.eclipse.sapphire.ui.forms.PropertiesViewContributionManager;
import org.eclipse.sapphire.ui.forms.PropertiesViewContributionPart;
import org.eclipse.sapphire.ui.forms.PropertiesViewContributorPart;
/**
* @author <a href="mailto:shenxue.zhou@oracle.com">Shenxue Zhou</a>
* @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a>
* @author <a href="mailto:ling.hao@oracle.com">Ling Hao</a>
*/
public class StandardDiagramConnectionPart
extends DiagramConnectionPart
implements PropertiesViewContributorPart
{
protected DiagramConnectionTemplate connectionTemplate;
protected IDiagramExplicitConnectionBindingDef bindingDef;
protected Element modelElement;
private ModelPath endpoint1Path;
private ModelPath endpoint2Path;
private Element srcNodeModel;
private Element targetNodeModel;
private ReferenceValue<?, ?> endpointReferenceValue1;
private ReferenceValue<?, ?> endpointReferenceValue2;
private Listener referenceServiceListener1;
private Listener referenceServiceListener2;
private PropertyDef endpoint1Property;
private PropertyDef endpoint2Property;
protected FunctionResult labelFunctionResult;
protected Value<?> labelProperty;
protected FunctionResult idFunctionResult;
protected Listener modelPropertyListener;
private PropertiesViewContributionManager propertiesViewContributionManager;
private List<Point> bendPoints = new ArrayList<Point>();
private Point labelPosition;
protected static final String CONNECTION_ID_SEPARATOR = "&";
public StandardDiagramConnectionPart() {}
public StandardDiagramConnectionPart(IDiagramExplicitConnectionBindingDef bindingDef, ModelPath endpoint1Path, ModelPath endpoint2Path)
{
this.bindingDef = bindingDef;
this.endpoint1Path = endpoint1Path;
this.endpoint2Path = endpoint2Path;
}
protected void initLabelId()
{
this.connectionTemplate = (DiagramConnectionTemplate) parent();
this.modelElement = getModelElement();
final IDiagramLabelDef labelDef = this.bindingDef.getLabel().content();
if (labelDef != null)
{
this.labelFunctionResult = initExpression
(
labelDef.getText().content(),
String.class,
null,
new Runnable()
{
public void run()
{
refreshLabel();
}
}
);
this.labelProperty = FunctionUtil.getFunctionProperty( this.modelElement, this.labelFunctionResult );
}
this.idFunctionResult = initExpression
(
this.bindingDef.getInstanceId().content(),
String.class,
null,
new Runnable()
{
public void run()
{
}
}
);
}
@Override
protected void init()
{
initLabelId();
this.srcNodeModel = resolveEndpoint(this.modelElement, this.endpoint1Path);
this.targetNodeModel = resolveEndpoint(this.modelElement, this.endpoint2Path);
this.endpoint1Property = this.modelElement.property(this.endpoint1Path).definition();
this.endpoint2Property = this.modelElement.property(this.endpoint2Path).definition();
// Add model property listener
this.modelPropertyListener = new FilteredListener<PropertyEvent>()
{
@Override
protected void handleTypedEvent( final PropertyEvent event )
{
handleModelPropertyChange( event );
}
};
addModelListener();
addReferenceServiceListeners();
}
public DiagramConnectionTemplate getDiagramConnectionTemplate()
{
return this.connectionTemplate;
}
@Override
public Element getLocalModelElement()
{
return this.modelElement;
}
public Element getEndpoint1()
{
return this.srcNodeModel;
}
public Element getEndpoint2()
{
return this.targetNodeModel;
}
public boolean canEditLabel()
{
return this.labelProperty != null;
}
public boolean removable()
{
return true;
}
public void remove()
{
final Element element = getLocalModelElement();
final ElementList<?> list = (ElementList<?>) element.parent();
list.remove(element);
pruneListParentIfNecessary(list);
}
private void pruneListParentIfNecessary(ElementList<?> list)
{
// For 1->n type connections, if the target node list is empty, we need to remove the surrounding
// element. See https://bugs.eclipse.org/bugs/show_bug.cgi?id=427710
if (list.isEmpty() && this.connectionTemplate.getConnectionType() == ConnectionType.OneToMany)
{
if (list.element().parent() instanceof ElementList<?>)
{
ElementList<?> grandParent = (ElementList<?>)list.element().parent();
grandParent.remove(list.element());
}
}
}
public String getLabel()
{
String label = null;
if( this.labelFunctionResult != null )
{
label = (String) this.labelFunctionResult.value();
}
return label;
}
public void setLabel(String newValue)
{
if (this.labelProperty != null)
{
this.labelProperty.write( newValue, true );
}
}
public IDiagramConnectionDef getConnectionDef()
{
return (IDiagramConnectionDef) definition();
}
public void refreshLabel()
{
notifyUpdateLabel();
}
public String getConnectionTypeId()
{
return this.definition.getId().content();
}
public String getInstanceId()
{
String id = null;
if( this.idFunctionResult != null )
{
id = (String) this.idFunctionResult.value();
}
return id;
}
public String getId()
{
StringBuffer buffer = new StringBuffer(getConnectionTypeId());
buffer.append(CONNECTION_ID_SEPARATOR);
String instanceId = getInstanceId();
if (instanceId != null && instanceId.length() > 0)
{
buffer.append(getInstanceId());
buffer.append(CONNECTION_ID_SEPARATOR);
}
List<StandardDiagramConnectionPart> connParts = getDiagramConnectionTemplate().getDiagramConnections(null);
int index = connParts.indexOf(this);
buffer.append(index);
return buffer.toString();
}
@Override
public Set<String> getActionContexts()
{
Set<String> contextSet = new HashSet<String>();
contextSet.add(SapphireActionSystem.CONTEXT_DIAGRAM_CONNECTION);
contextSet.add(SapphireActionSystem.CONTEXT_DIAGRAM_CONNECTION_HIDDEN);
return contextSet;
}
@Override
public void dispose()
{
super.dispose();
if (this.labelFunctionResult != null)
{
this.labelFunctionResult.dispose();
}
if (this.idFunctionResult != null)
{
this.idFunctionResult.dispose();
}
removeModelListener();
}
protected void resetEndpoint1(DiagramNodePart newSrcNode)
{
this.srcNodeModel = newSrcNode.getLocalModelElement();
String endpoint1Value = this.connectionTemplate.getSerializedEndpoint1(newSrcNode);
this.connectionTemplate.setSerializedEndpoint1(this.modelElement, endpoint1Value);
}
protected void resetEndpoint2(DiagramNodePart newTargetNode)
{
this.targetNodeModel = newTargetNode.getLocalModelElement();
String endpoint2Value = this.connectionTemplate.getSerializedEndpoint2(newTargetNode);
this.connectionTemplate.setSerializedEndpoint2(this.modelElement, endpoint2Value);
}
protected Element resolveEndpoint(Element modelElement, ModelPath endpointPath)
{
if (endpointPath.length() == 1)
{
String propertyName = ((ModelPath.PropertySegment)endpointPath.head()).getPropertyName();
PropertyDef modelProperty = resolve(modelElement, propertyName);
if (!(modelProperty instanceof ValueProperty))
{
throw new RuntimeException( "Property " + propertyName + " not a ValueProperty");
}
ValueProperty property = (ValueProperty)modelProperty;
Value<?> valObj = modelElement.property(property);
if (!(valObj instanceof ReferenceValue))
{
throw new RuntimeException( "Property " + propertyName + " value not a reference");
}
ReferenceValue<?,?> refVal = (ReferenceValue<?,?>)valObj;
Object targetObj = refVal.target();
if (targetObj == null)
{
if (refVal.text() != null)
{
SapphireDiagramEditorPagePart diagramEditorPart = this.getDiagramConnectionTemplate().getDiagramEditor();
DiagramNodePart targetNode = diagramEditorPart.getNode(refVal.text());
if (targetNode != null)
{
targetObj = targetNode.getLocalModelElement();
}
}
}
return (Element)targetObj;
}
else
{
ModelPath.Segment head = endpointPath.head();
if( head instanceof ParentElementSegment )
{
final Property parent = modelElement.parent();
if( parent == null )
{
throw new RuntimeException("Invalid model path: " + endpointPath);
}
return resolveEndpoint(parent.element(), endpointPath.tail());
}
else
{
throw new RuntimeException("Invalid model path: " + endpointPath);
}
}
}
protected ReferenceValue<?, ?> resolveEndpointReferenceValue(Element modelElement, ModelPath endpointPath)
{
if (endpointPath.length() == 1)
{
String propertyName = ((ModelPath.PropertySegment)endpointPath.head()).getPropertyName();
PropertyDef modelProperty = resolve(modelElement, propertyName);
if (!(modelProperty instanceof ValueProperty))
{
throw new RuntimeException( "Property " + propertyName + " not a ValueProperty");
}
ValueProperty property = (ValueProperty)modelProperty;
Value<?> valObj = modelElement.property(property);
if (!(valObj instanceof ReferenceValue))
{
throw new RuntimeException( "Property " + propertyName + " value not a reference");
}
ReferenceValue<?,?> refVal = (ReferenceValue<?,?>)valObj;
return refVal;
}
else
{
ModelPath.Segment head = endpointPath.head();
if( head instanceof ParentElementSegment )
{
final Property parent = modelElement.parent();
if( parent == null )
{
throw new RuntimeException("Invalid model path: " + endpointPath);
}
return resolveEndpointReferenceValue(parent.element(), endpointPath.tail());
}
else
{
throw new RuntimeException("Invalid model path: " + endpointPath);
}
}
}
protected void setModelProperty(final Element modelElement,
String propertyName, Object value)
{
if (propertyName != null)
{
final ElementType type = modelElement.type();
final PropertyDef property = type.property( propertyName );
if( property == null )
{
throw new RuntimeException( "Could not find property " + propertyName + " in " + type.getQualifiedName() );
}
if (!(property instanceof ValueProperty))
{
throw new RuntimeException( "Property " + propertyName + " not a ValueProperty");
}
modelElement.property( (ValueProperty) property ).write( value, true );
}
}
protected void setModelProperty(Element modelElement, ModelPath propertyPath, Object value)
{
if (propertyPath.length() == 1)
{
String propertyName = ((ModelPath.PropertySegment)propertyPath.head()).getPropertyName();
setModelProperty(modelElement, propertyName, value);
}
else
{
if (propertyPath.head() instanceof ModelPath.ParentElementSegment)
{
final Property parent = modelElement.parent();
setModelProperty(parent.element(), propertyPath.tail(), value);
}
}
}
public void addModelListener()
{
this.modelElement.attach(this.modelPropertyListener, this.endpoint1Path);
this.modelElement.attach(this.modelPropertyListener, this.endpoint2Path);
}
public void removeModelListener()
{
this.modelElement.detach(this.modelPropertyListener, this.endpoint1Path);
this.modelElement.detach(this.modelPropertyListener, this.endpoint2Path);
}
public void addReferenceServiceListeners()
{
this.endpointReferenceValue1 = resolveEndpointReferenceValue(modelElement, this.endpoint1Path);
if (this.endpointReferenceValue1 != null)
{
ReferenceService<?> refService = endpointReferenceValue1.service(ReferenceService.class);
this.referenceServiceListener1 = new Listener()
{
@Override
public void handle( Event event )
{
handleEndpoint1ReferenceChange();
}
};
if (refService != null)
{
refService.attach(this.referenceServiceListener1);
}
}
this.endpointReferenceValue2 = resolveEndpointReferenceValue(modelElement, this.endpoint2Path);
if (this.endpointReferenceValue2 != null)
{
ReferenceService<?> refService = endpointReferenceValue2.service(ReferenceService.class);
this.referenceServiceListener2 = new Listener()
{
@Override
public void handle( Event event )
{
handleEndpoint2ReferenceChange();
}
};
if (refService != null)
{
refService.attach(this.referenceServiceListener2);
}
}
}
public StandardDiagramConnectionPart reconnect(DiagramNodePart newSrcNode, DiagramNodePart newTargetNode)
{
/**
* Optimization: no need to reconnect if the connection's endpoint model
* elements don't change. Connection listens on endpoint's ReferenceService and its
* endpoint model elements are refreshed when the attaching nodes's elements change.
*/
if (newSrcNode != null && newSrcNode.getLocalModelElement() == getEndpoint1() &&
newTargetNode != null && newTargetNode.getLocalModelElement() == getEndpoint2())
{
return this;
}
if (newSrcNode != null && newTargetNode != null)
{
StandardDiagramConnectionPart newConnPart = getDiagramConnectionTemplate().createNewDiagramConnection(newSrcNode, newTargetNode);
final Element oldConnElement = this.getLocalModelElement();
newConnPart.getLocalModelElement().copy(oldConnElement);
// Bug 382912 - Reconnecting an existing connection adds a bend point
// After the copy, connection endpoint event is triggered which causes SapphireConnectionRouter
// to be called. Since the old connection hasn't been deleted, a default bend point will be added.
//newConnPart.removeAllBendpoints();
newConnPart.resetBendpoints(getBendpoints());
if (newSrcNode.getLocalModelElement() != getEndpoint1())
{
newConnPart.resetEndpoint1(newSrcNode);
}
if (newTargetNode.getLocalModelElement() != getEndpoint2())
{
newConnPart.resetEndpoint2(newTargetNode);
}
final ElementList<?> list = (ElementList<?>) oldConnElement.parent();
list.remove(oldConnElement);
pruneListParentIfNecessary(list);
return newConnPart;
}
return null;
}
protected void handleModelPropertyChange(final PropertyEvent event)
{
final PropertyDef property = event.property().definition();
if (property.name().equals(this.endpoint1Property.name()) ||
property.name().equals(this.endpoint2Property.name()))
{
boolean sourceChange = property.name().equals(this.endpoint1Property.name()) ? true : false;
if (sourceChange)
{
this.srcNodeModel = resolveEndpoint(this.modelElement, this.endpoint1Path);
}
else
{
this.targetNodeModel = resolveEndpoint(this.modelElement, this.endpoint2Path);
}
notifyConnectionEndpointUpdate();
}
}
protected void handleEndpoint1ReferenceChange()
{
Element newSourceModel = resolveEndpoint(this.modelElement, this.endpoint1Path);
if (newSourceModel != this.srcNodeModel)
{
this.srcNodeModel = newSourceModel;
notifyConnectionEndpointUpdate();
}
}
protected void handleEndpoint2ReferenceChange()
{
Element newTargetModel = resolveEndpoint(this.modelElement, this.endpoint2Path);
if (newTargetModel != this.targetNodeModel)
{
this.targetNodeModel = newTargetModel;
notifyConnectionEndpointUpdate();
}
}
protected void notifyUpdateLabel()
{
ConnectionLabelEvent labelEvent = new ConnectionLabelEvent(this, false);
this.broadcast(labelEvent);
}
protected void notifyConnectionEndpointUpdate()
{
ConnectionEndpointsEvent event = new ConnectionEndpointsEvent(this);
this.broadcast(event);
}
protected void notifyAddBendpoint()
{
ConnectionBendpointsEvent event = new ConnectionBendpointsEvent(this);
this.broadcast(event);
}
protected void notifyRemoveBendpoint()
{
ConnectionBendpointsEvent event = new ConnectionBendpointsEvent(this);
this.broadcast(event);
}
protected void notifyMoveBendpoint()
{
ConnectionBendpointsEvent event = new ConnectionBendpointsEvent(this);
this.broadcast(event);
}
protected void notifyResetBendpoints()
{
ConnectionBendpointsEvent event = new ConnectionBendpointsEvent(this, true);
this.broadcast(event);
}
protected void notifyMoveConnectionLabel()
{
ConnectionLabelEvent labelEvent = new ConnectionLabelEvent(this, true);
this.broadcast(labelEvent);
}
public PropertiesViewContributionPart getPropertiesViewContribution()
{
if( this.propertiesViewContributionManager == null )
{
this.propertiesViewContributionManager = new PropertiesViewContributionManager( this, getLocalModelElement(), getConnectionDef() );
}
return this.propertiesViewContributionManager.getPropertiesViewContribution();
}
public void addBendpoint(int index, int x, int y)
{
this.bendPoints.add(index, new Point(x, y));
notifyAddBendpoint();
}
public void removeBendpoint(int index)
{
this.bendPoints.remove(index);
notifyRemoveBendpoint();
}
public void removeAllBendpoints()
{
this.bendPoints.clear();
notifyRemoveBendpoint();
}
public void updateBendpoint(int index, int x, int y)
{
if (index < this.bendPoints.size())
{
this.bendPoints.set(index, new Point(x, y));
}
notifyMoveBendpoint();
}
public void resetBendpoints(List<Point> bendpoints)
{
boolean changed = false;
if (bendpoints.size() != this.bendPoints.size())
{
changed = true;
}
else
{
for (int i = 0; i < bendpoints.size(); i++)
{
Point newPt = bendpoints.get(i);
Point oldPt = this.bendPoints.get(i);
if (newPt.getX() != oldPt.getX() || newPt.getY() != oldPt.getY())
{
changed = true;
break;
}
}
}
if (changed)
{
this.bendPoints.clear();
this.bendPoints.addAll(bendpoints);
notifyResetBendpoints();
}
}
public List<Point> getBendpoints()
{
List<Point> bendPoints = new ArrayList<Point>();
bendPoints.addAll(this.bendPoints);
return bendPoints;
}
public Point getLabelPosition()
{
return this.labelPosition;
}
public void setLabelPosition(Point newPos)
{
boolean changed = false;
if (this.labelPosition == null && newPos != null)
{
this.labelPosition = new Point(newPos);
changed = true;
}
else if (this.labelPosition != null && newPos == null)
{
this.labelPosition = null;
changed = true;
}
else if (this.labelPosition != null && newPos != null && !this.labelPosition.equals(newPos))
{
this.labelPosition.setX(newPos.getX());
this.labelPosition.setY(newPos.getY());
changed = true;
}
if (changed)
{
notifyMoveConnectionLabel();
}
}
}