/******************************************************************************
* 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 - [378756] Convert ModelElementListener and ModelPropertyListener to common listener infrastructure
* Konstantin Komissarchik - [376245] Revert action in StructuredTextEditor does not revert diagram nodes and connections in SapphireDiagramEditor
* Konstantin Komissarchik - [381233] IllegalStateException in ServiceContext when modifying xml in source editor in Architecture sample
* Ling Hao - [383924] Flexible diagram node shapes
******************************************************************************/
package org.eclipse.sapphire.samples.architecture.internal;
import static java.lang.Math.min;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.sapphire.Element;
import org.eclipse.sapphire.ElementList;
import org.eclipse.sapphire.Event;
import org.eclipse.sapphire.FilteredListener;
import org.eclipse.sapphire.Listener;
import org.eclipse.sapphire.PropertyEvent;
import org.eclipse.sapphire.samples.architecture.ArchitectureSketch;
import org.eclipse.sapphire.samples.architecture.Component;
import org.eclipse.sapphire.samples.architecture.ComponentDependency;
import org.eclipse.sapphire.samples.architecture.ConnectionBendpoint;
import org.eclipse.sapphire.ui.Bounds;
import org.eclipse.sapphire.ui.Point;
import org.eclipse.sapphire.ui.diagram.ConnectionAddEvent;
import org.eclipse.sapphire.ui.diagram.ConnectionBendpointsEvent;
import org.eclipse.sapphire.ui.diagram.ConnectionDeleteEvent;
import org.eclipse.sapphire.ui.diagram.ConnectionService;
import org.eclipse.sapphire.ui.diagram.DiagramConnectionPart;
import org.eclipse.sapphire.ui.diagram.editor.DiagramNodeAddEvent;
import org.eclipse.sapphire.ui.diagram.editor.DiagramNodeBounds;
import org.eclipse.sapphire.ui.diagram.editor.DiagramNodeDeleteEvent;
import org.eclipse.sapphire.ui.diagram.editor.DiagramNodeEvent;
import org.eclipse.sapphire.ui.diagram.editor.DiagramNodeMoveEvent;
import org.eclipse.sapphire.ui.diagram.editor.DiagramNodePart;
import org.eclipse.sapphire.ui.diagram.editor.DiagramPageEvent;
import org.eclipse.sapphire.ui.diagram.editor.SapphireDiagramEditorPagePart;
import org.eclipse.sapphire.ui.diagram.editor.SapphireDiagramEditorPagePart.PostAutoLayoutEvent;
import org.eclipse.sapphire.ui.diagram.editor.SapphireDiagramEditorPagePart.PreAutoLayoutEvent;
import org.eclipse.sapphire.ui.diagram.layout.ConnectionHashKey;
import org.eclipse.sapphire.ui.diagram.layout.DiagramLayoutPersistenceService;
/**
* @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 ArchitectureDiagramLayoutPersistenceService extends DiagramLayoutPersistenceService
{
private ArchitectureSketch architecture;
private Listener diagramPartListener;
private Listener connectionPartListener;
private Listener componentListener;
private Listener componentDependencyListener;
private Map<String, DiagramNodeBounds> nodeBounds;
private Map<ConnectionHashKey, List<Point>> connectionBendPoints;
private boolean dirty;
private boolean autoLayout = false;
@Override
protected void init()
{
super.init();
this.architecture = (ArchitectureSketch)context( SapphireDiagramEditorPagePart.class ).getLocalModelElement();
this.nodeBounds = new HashMap<String, DiagramNodeBounds>();
this.connectionBendPoints = new HashMap<ConnectionHashKey, List<Point>>();
this.dirty = false;
this.connectionPartListener = new FilteredListener<ConnectionBendpointsEvent>()
{
@Override
protected void handleTypedEvent(ConnectionBendpointsEvent event)
{
if (event.reset())
{
if (autoLayout)
{
addConnectionToPersistenceCache(event.part());
refreshDirtyState();
}
else
{
write(event.part());
}
}
else
{
write(event.part());
}
}
};
load();
refreshPersistedPartsCache();
addDiagramPartListener();
addModelListeners();
}
private void setGridVisible(boolean visible)
{
this.architecture.setShowGrid(visible);
}
private void setGuidesVisible(boolean visible)
{
this.architecture.setShowGuides(visible);
}
private void read(DiagramNodePart nodePart)
{
Component component = (Component)nodePart.getLocalModelElement();
if (!component.disposed())
{
String nodeId = nodePart.getId();
if (this.nodeBounds.containsKey(nodeId) && this.nodeBounds.get(nodeId) != null)
{
nodePart.setNodeBounds(this.nodeBounds.get(nodeId));
}
}
}
private void write(DiagramNodePart nodePart)
{
Component component = (Component)nodePart.getLocalModelElement();
if (!component.disposed())
{
if (isNodeLayoutChanged(nodePart))
{
this.architecture.detach(this.componentListener, "/Components/Position/*");
writeComponentBounds(component, nodePart);
this.architecture.attach(this.componentListener, "/Components/Position/*");
}
refreshDirtyState();
}
}
public DiagramConnectionInfo read(DiagramConnectionPart connPart)
{
ConnectionHashKey key = ConnectionHashKey.createKey(connPart);
if (this.connectionBendPoints.containsKey(key))
{
DiagramConnectionInfo connectionInfo = new DiagramConnectionInfo(this.connectionBendPoints.get(key));
return connectionInfo;
}
else
{
return null;
}
}
private void write(DiagramConnectionPart connPart)
{
ComponentDependency dependency = (ComponentDependency)connPart.getLocalModelElement();
if (!dependency.disposed())
{
if (isConnectionLayoutChanged(connPart))
{
this.architecture.detach(this.componentDependencyListener, "/Components/Dependencies/ConnectionBendpoints/*");
writeDependencyBendPoints(dependency, connPart);
this.architecture.attach(this.componentDependencyListener, "/Components/Dependencies/ConnectionBendpoints/*");
}
refreshDirtyState();
}
}
@Override
public void dispose()
{
if (this.diagramPartListener != null)
{
context( SapphireDiagramEditorPagePart.class ).detach(this.diagramPartListener);
}
if (this.componentListener != null)
{
this.architecture.detach(this.componentListener, "/Components/Position/*");
}
if (this.componentDependencyListener != null)
{
this.architecture.detach(this.componentDependencyListener, "/Components/Dependencies/ConnectionBendpoints/*");
}
}
private void load()
{
context( SapphireDiagramEditorPagePart.class ).setGridVisible(this.architecture.isShowGrid().content());
context( SapphireDiagramEditorPagePart.class ).setShowGuides(this.architecture.isShowGuides().content());
ElementList<Component> components = this.architecture.getComponents();
ConnectionService connService = context( SapphireDiagramEditorPagePart.class ).service(ConnectionService.class);
for (Component component : components)
{
DiagramNodePart nodePart = context( SapphireDiagramEditorPagePart.class ).getDiagramNodePart(component);
if (nodePart != null)
{
DiagramNodeBounds bounds = null;
bounds = new DiagramNodeBounds(component.getPosition().getX().content(),
component.getPosition().getY().content(), -1, -1,
false, false);
nodePart.setNodeBounds(bounds);
// load the embedded connection layout
ElementList<ComponentDependency> dependencies = component.getDependencies();
for (ComponentDependency dependency : dependencies)
{
DiagramConnectionPart connPart = getConnectionPart(connService, dependency);
if (connPart != null)
{
ElementList<ConnectionBendpoint> bendpoints = dependency.getConnectionBendpoints();
if (bendpoints.size() > 0)
{
int index = 0;
for (ConnectionBendpoint bendpoint : bendpoints)
{
connPart.addBendpoint(index++, bendpoint.getX().content(),
bendpoint.getY().content());
}
}
}
}
}
}
// Listen on existing connection parts
for (DiagramConnectionPart connPart : connService.list())
{
connPart.attach(this.connectionPartListener);
}
}
private void handleNodeLayoutChange(Component component)
{
DiagramNodePart nodePart = context( SapphireDiagramEditorPagePart.class ).getDiagramNodePart(component);
DiagramNodeBounds nodeBounds = new DiagramNodeBounds(component.getPosition().getX().content(),
component.getPosition().getY().content());
nodePart.setNodeBounds(nodeBounds);
}
private void handleConnectionBendpointChange(ComponentDependency componentDependency)
{
ConnectionService connService = context(SapphireDiagramEditorPagePart.class).service(ConnectionService.class);
DiagramConnectionPart connPart = getConnectionPart(connService, componentDependency);
if (connPart != null)
{
List<Point> bendpoints = new ArrayList<Point>();
for (ConnectionBendpoint bendpoint : componentDependency.getConnectionBendpoints())
{
bendpoints.add(new Point(bendpoint.getX().content(), bendpoint.getY().content()));
}
connPart.resetBendpoints(bendpoints);
}
}
private void addNodeToPersistenceCache(DiagramNodePart nodePart)
{
String nodeId = nodePart.getId();
this.nodeBounds.put(nodeId, nodePart.getNodeBounds());
}
private void addConnectionToPersistenceCache(DiagramConnectionPart connPart)
{
ConnectionHashKey connKey = ConnectionHashKey.createKey(connPart);
this.connectionBendPoints.put(connKey, connPart.getBendpoints());
}
private void addDiagramPartListener()
{
this.diagramPartListener = new Listener()
{
@Override
public void handle( final Event event )
{
if ( event instanceof DiagramNodeEvent )
{
handleDiagramNodeEvent((DiagramNodeEvent)event);
}
else if ( event instanceof DiagramPageEvent )
{
handleDiagramPageEvent((DiagramPageEvent)event);
}
else if (event instanceof PreAutoLayoutEvent)
{
autoLayout = true;
}
else if (event instanceof PostAutoLayoutEvent)
{
autoLayout = false;
}
else if (event instanceof ConnectionAddEvent)
{
handleConnectionAddEvent((ConnectionAddEvent)event);
}
else if (event instanceof ConnectionDeleteEvent)
{
handleConnectionDeleteEvent((ConnectionDeleteEvent)event);
}
}
};
context( SapphireDiagramEditorPagePart.class ).attach( this.diagramPartListener );
}
private void handleDiagramNodeEvent(DiagramNodeEvent event) {
DiagramNodePart nodePart = (DiagramNodePart)event.part();
if (event instanceof DiagramNodeAddEvent)
{
read(nodePart);
}
else if (event instanceof DiagramNodeDeleteEvent)
{
refreshDirtyState();
}
else if (event instanceof DiagramNodeMoveEvent)
{
DiagramNodeBounds nodeBounds = nodePart.getNodeBounds();
if (nodeBounds.isAutoLayout())
{
// need to add the node bounds to the persistence cache so that "revert" could work
addNodeToPersistenceCache(nodePart);
refreshDirtyState();
}
else if (!nodeBounds.isDefaultPosition())
{
write((DiagramNodePart)event.part());
}
}
}
protected void handleConnectionAddEvent(ConnectionAddEvent event)
{
DiagramConnectionPart connPart = event.part();
connPart.attach(this.connectionPartListener);
DiagramConnectionInfo connectionInfo = read(connPart);
if (connectionInfo != null)
{
connPart.resetBendpoints(connectionInfo.getBendPoints());
}
}
protected void handleConnectionDeleteEvent(ConnectionDeleteEvent event)
{
refreshDirtyState();
}
private void handleDiagramPageEvent(DiagramPageEvent event) {
SapphireDiagramEditorPagePart diagramPart = (SapphireDiagramEditorPagePart)event.part();
switch(event.getDiagramPageEventType()) {
case GridStateChange:
setGridVisible(diagramPart.isGridVisible());
break;
case GuideStateChange:
setGuidesVisible(diagramPart.isShowGuides());
break;
case DiagramSave:
doSave();
break;
default:
break;
}
}
private void addModelListeners()
{
this.componentListener = new FilteredListener<PropertyEvent>()
{
@Override
protected void handleTypedEvent( final PropertyEvent event )
{
Component component = event.property().element().nearest(Component.class);
if (component != null)
{
handleNodeLayoutChange(component);
}
}
};
this.componentDependencyListener = new FilteredListener<PropertyEvent>()
{
@Override
protected void handleTypedEvent( final PropertyEvent event )
{
ComponentDependency componentDependency = event.property().element().nearest(ComponentDependency.class);
if (componentDependency != null)
{
handleConnectionBendpointChange(componentDependency);
}
}
};
this.architecture.attach(this.componentListener, "/Components/Position/*");
this.architecture.attach(this.componentDependencyListener, "/Components/Dependencies/ConnectionBendpoints/*");
}
private void writeComponentBounds(Component component, DiagramNodePart node)
{
Bounds bounds = node.getNodeBounds();
if (bounds.getX() != component.getPosition().getX().content())
{
component.getPosition().setX(bounds.getX());
}
if (bounds.getY() != component.getPosition().getY().content())
{
component.getPosition().setY(bounds.getY());
}
}
private void writeDependencyBendPoints(ComponentDependency dependency, DiagramConnectionPart connPart)
{
final ElementList<ConnectionBendpoint> bpInModelList = dependency.getConnectionBendpoints();
final int bpInModelSize = bpInModelList.size();
final List<Point> bpInPartList = connPart.getBendpoints();
final int bpInPartSize = bpInPartList.size();
for( int i = 0, n = min( bpInModelSize, bpInPartSize ); i < n; i++ )
{
final ConnectionBendpoint bpInModel = bpInModelList.get( i );
final Point bpInPart = bpInPartList.get( i );
if (bpInModel.getX().content() != bpInPart.getX())
{
bpInModel.setX( bpInPart.getX() );
}
if (bpInModel.getY().content() != bpInPart.getY())
{
bpInModel.setY( bpInPart.getY() );
}
}
if( bpInModelSize < bpInPartSize )
{
for( int i = bpInModelSize; i < bpInPartSize; i++ )
{
final ConnectionBendpoint bpInModel = bpInModelList.insert();
final Point bpInPart = bpInPartList.get( i );
bpInModel.setX( bpInPart.getX() );
bpInModel.setY( bpInPart.getY() );
}
}
else if( bpInModelSize > bpInPartSize )
{
for( int i = bpInModelSize - 1; i >= bpInPartSize; i-- )
{
bpInModelList.remove( i );
}
}
}
private void doSave()
{
refreshPersistedPartsCache();
// For nodes that are placed using default node positions and connection bend points that
// are calculated using connection router, we don't modify the corresponding model properties
// in order to allow "revert" in source editor to work correctly.
// So we need to do an explicit save of the node bounds and connection bend points here.
this.architecture.detach(this.componentListener, "/Components/Position/*");
this.architecture.detach(this.componentDependencyListener, "/Components/Dependencies/ConnectionBendpoints/*");
for (DiagramNodePart nodePart : context( SapphireDiagramEditorPagePart.class ).getNodes())
{
Component component = (Component)nodePart.getLocalModelElement();
if (!component.disposed())
{
writeComponentBounds(component, nodePart);
}
}
SapphireDiagramEditorPagePart diagramPart = context( SapphireDiagramEditorPagePart.class );
ConnectionService connService = diagramPart.service(ConnectionService.class);
for (DiagramConnectionPart connPart : connService.list())
{
ComponentDependency dependency = (ComponentDependency)connPart.getLocalModelElement();
if (!dependency.disposed())
{
writeDependencyBendPoints(dependency, connPart);
}
}
this.architecture.attach(this.componentListener, "/Components/Position/*");
this.architecture.attach(this.componentDependencyListener, "/Components/Dependencies/ConnectionBendpoints/*");
}
private boolean isNodeLayoutChanged(DiagramNodePart nodePart)
{
DiagramNodeBounds newBounds = nodePart.getNodeBounds();
boolean changed = false;
String nodeId = (nodePart).getId();
if (this.nodeBounds.containsKey(nodeId))
{
DiagramNodeBounds oldBounds = this.nodeBounds.get(nodeId);
if (!newBounds.equals(oldBounds))
{
changed = true;
}
}
else
{
changed = true;
}
return changed;
}
private boolean isConnectionLayoutChanged(DiagramConnectionPart connPart)
{
// Detect whether the connection bendpoints have been changed.
List<Point> bendpoints = connPart.getBendpoints();
ConnectionHashKey key = ConnectionHashKey.createKey(connPart);
boolean changed = false;
if (this.connectionBendPoints.containsKey(key))
{
List<Point> oldBendpoints = this.connectionBendPoints.get(key);
if (bendpoints.size() != oldBendpoints.size())
{
changed = true;
}
else
{
for (int i = 0; i < bendpoints.size(); i++)
{
Point newPt = bendpoints.get(i);
Point oldPt = oldBendpoints.get(i);
if (newPt.getX() != oldPt.getX() || newPt.getY() != oldPt.getY())
{
changed = true;
break;
}
}
}
if (!bendpoints.equals(oldBendpoints))
{
changed = true;
}
}
else
{
changed = true;
}
return changed;
}
private boolean isDiagramLayoutChanged()
{
boolean changed = false;
if (!context(SapphireDiagramEditorPagePart.class).disposed())
{
ConnectionService connService = context(SapphireDiagramEditorPagePart.class).service(ConnectionService.class);
for (DiagramNodePart nodePart : context( SapphireDiagramEditorPagePart.class ).getNodes())
{
if (!nodePart.getLocalModelElement().disposed() && isNodeLayoutChanged(nodePart))
{
changed = true;
break;
}
}
for (DiagramConnectionPart connPart : connService.list())
{
if (!connPart.getLocalModelElement().disposed() && isConnectionLayoutChanged(connPart))
{
changed = true;
break;
}
}
}
return changed;
}
@Override
public boolean dirty()
{
return this.dirty;
}
private void refreshDirtyState()
{
final boolean after = isDiagramLayoutChanged();
if( this.dirty != after )
{
final boolean before = this.dirty;
this.dirty = after;
broadcast( new DirtyStateEvent( this, before, after ) );
}
}
private void refreshPersistedPartsCache()
{
this.nodeBounds.clear();
this.connectionBendPoints.clear();
ConnectionService connService = context(SapphireDiagramEditorPagePart.class).service(ConnectionService.class);
for (DiagramConnectionPart connPart : connService.list())
{
addConnectionToPersistenceCache(connPart);
}
for (DiagramNodePart nodePart : context( SapphireDiagramEditorPagePart.class ).getNodes())
{
addNodeToPersistenceCache(nodePart);
}
}
private DiagramConnectionPart getConnectionPart(ConnectionService connService, Element element)
{
for (DiagramConnectionPart connPart : connService.list())
{
if (connPart.getLocalModelElement() == element)
{
return connPart;
}
}
return null;
}
}