/******************************************************************************
* 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:
* Konstantin Komissarchik - initial implementation and ongoing maintenance
******************************************************************************/
package org.eclipse.sapphire.ui.forms;
import static org.eclipse.sapphire.ui.forms.swt.SwtUtil.runOnDisplayThread;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.sapphire.Color;
import org.eclipse.sapphire.Element;
import org.eclipse.sapphire.ElementHandle;
import org.eclipse.sapphire.ElementList;
import org.eclipse.sapphire.Event;
import org.eclipse.sapphire.FilteredListener;
import org.eclipse.sapphire.ImageData;
import org.eclipse.sapphire.ImpliedElementProperty;
import org.eclipse.sapphire.Listener;
import org.eclipse.sapphire.ListenerContext;
import org.eclipse.sapphire.LocalizableText;
import org.eclipse.sapphire.Property;
import org.eclipse.sapphire.PropertyContentEvent;
import org.eclipse.sapphire.PropertyDef;
import org.eclipse.sapphire.PropertyValidationEvent;
import org.eclipse.sapphire.Text;
import org.eclipse.sapphire.java.JavaType;
import org.eclipse.sapphire.modeling.CapitalizationType;
import org.eclipse.sapphire.modeling.ModelPath;
import org.eclipse.sapphire.modeling.Status;
import org.eclipse.sapphire.modeling.el.AndFunction;
import org.eclipse.sapphire.modeling.el.Function;
import org.eclipse.sapphire.modeling.el.FunctionContext;
import org.eclipse.sapphire.modeling.el.FunctionResult;
import org.eclipse.sapphire.modeling.el.Literal;
import org.eclipse.sapphire.modeling.localization.LocalizationService;
import org.eclipse.sapphire.ui.ISapphirePart;
import org.eclipse.sapphire.ui.PartValidationEvent;
import org.eclipse.sapphire.ui.PartVisibilityEvent;
import org.eclipse.sapphire.ui.SapphireActionSystem;
import org.eclipse.sapphire.ui.SapphirePart;
import org.eclipse.sapphire.ui.def.ISapphireParam;
import org.eclipse.sapphire.util.ListFactory;
/**
* @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a>
*/
public final class MasterDetailsContentNodePart
extends SapphirePart
implements PropertiesViewContributorPart
{
private static final ImageData IMG_CONTAINER_NODE
= ImageData.readFromClassLoader( MasterDetailsContentNodePart.class, "ContainerNode.png" ).required();
private static final ImageData IMG_LEAF_NODE
= ImageData.readFromClassLoader( MasterDetailsContentNodePart.class, "LeafNode.png" ).required();
@Text( "Could not resolve node \"{0}\"." )
private static LocalizableText couldNotResolveNode;
@Text( "Could not resolve section = \"{0}\"." )
private static LocalizableText couldNotResolveSection;
static
{
LocalizableText.init( MasterDetailsContentNodePart.class );
}
private MasterDetailsContentOutline contentTree;
private MasterDetailsContentNodeDef definition;
private Element modelElement;
private ElementHandle<?> modelElementProperty;
private MasterDetailsContentNodePart parentNode;
private FunctionResult labelFunctionResult;
private ImageManager imageManager;
private List<TextDecoration> decorations;
private Listener childPartListener;
private List<Object> rawChildren;
private MasterDetailsContentNodeList nodes;
private List<SectionPart> sections;
private PropertiesViewContributionManager propertiesViewContributionManager;
private boolean expanded;
private boolean transformLabelCase = true;
private final Function nodeFactoryVisibleFunction;
public MasterDetailsContentNodePart()
{
this( null );
}
public MasterDetailsContentNodePart( final Function nodeFactoryVisibleFunction )
{
this.nodeFactoryVisibleFunction = nodeFactoryVisibleFunction;
}
@Override
protected void init()
{
super.init();
final ISapphirePart parent = parent();
if( parent instanceof MasterDetailsContentNodePart )
{
this.parentNode = (MasterDetailsContentNodePart) parent;
}
else
{
this.parentNode = null;
}
this.contentTree = nearest( MasterDetailsEditorPagePart.class ).outline();
this.definition = (MasterDetailsContentNodeDef) super.definition;
final ImpliedElementProperty modelElementProperty = (ImpliedElementProperty) resolve( this.definition.getProperty().content() );
if( modelElementProperty != null )
{
this.modelElementProperty = getModelElement().property( modelElementProperty );
this.modelElement = this.modelElementProperty.content();
}
else
{
this.modelElement = getModelElement();
}
this.expanded = false;
this.childPartListener = new Listener()
{
@Override
public void handle( final Event event )
{
if( event instanceof PartValidationEvent || event instanceof PartVisibilityEvent )
{
refreshValidation();
}
}
};
// Label
this.labelFunctionResult = initExpression
(
this.definition.getLabel().content(),
String.class,
null,
new Runnable()
{
public void run()
{
broadcast( new LabelChangedEvent( MasterDetailsContentNodePart.this ) );
}
}
);
// Image
final Literal defaultImageLiteral = Literal.create( ( this.definition.getChildNodes().isEmpty() ? IMG_LEAF_NODE : IMG_CONTAINER_NODE ) );
final Function imageFunction = this.definition.getImage().content();
this.imageManager = new ImageManager( imageFunction, defaultImageLiteral );
// Decorations
final ListFactory<TextDecoration> decorationsListFactory = ListFactory.start();
for( final TextDecorationDef ddef : this.definition.getDecorations() )
{
final FunctionResult text = initExpression
(
ddef.getText().content(),
String.class,
null,
new Runnable()
{
public void run()
{
broadcast( new DecorationEvent( MasterDetailsContentNodePart.this ) );
}
}
);
final FunctionResult color = initExpression
(
ddef.getColor().content(),
Color.class,
null,
new Runnable()
{
public void run()
{
broadcast( new DecorationEvent( MasterDetailsContentNodePart.this ) );
}
}
);
decorationsListFactory.add( new TextDecoration( text, color ) );
}
this.decorations = decorationsListFactory.result();
// Sections and Child Nodes
this.rawChildren = new ArrayList<Object>();
final ListFactory<SectionPart> sectionsListFactory = ListFactory.start();
for( FormComponentDef s : this.definition.getSections() )
{
final SectionDef sectionDefinition;
final Map<String,String> sectionParams;
if( s instanceof SectionDef )
{
sectionDefinition = (SectionDef) s;
sectionParams = this.params;
}
else if( s instanceof SectionRef )
{
final SectionRef sectionReference = (SectionRef) s;
sectionDefinition = sectionReference.getSection().target();
if( sectionDefinition == null )
{
final String msg = couldNotResolveSection.format( sectionReference.getSection().text() );
throw new RuntimeException( msg );
}
sectionParams = new HashMap<String,String>( this.params );
for( ISapphireParam param : sectionReference.getParams() )
{
final String paramName = param.getName().text();
final String paramValue = param.getValue().text();
if( paramName != null && paramValue != null )
{
sectionParams.put( paramName, paramValue );
}
}
}
else
{
throw new IllegalStateException();
}
final SectionPart section = new SectionPart();
section.init( this, this.modelElement, sectionDefinition, sectionParams );
section.initialize();
section.attach( this.childPartListener );
sectionsListFactory.add( section );
}
this.sections = sectionsListFactory.result();
for( MasterDetailsContentNodeChildDef entry : this.definition.getChildNodes() )
{
final Map<String,String> params = new HashMap<String,String>( this.params );
if( entry instanceof MasterDetailsContentNodeRef )
{
final MasterDetailsContentNodeRef inc = (MasterDetailsContentNodeRef) entry;
entry = inc.resolve();
if( entry == null )
{
final String msg = couldNotResolveNode.format( inc.getPart() );
throw new RuntimeException( msg );
}
for( ISapphireParam param : inc.getParams() )
{
final String paramName = param.getName().text();
final String paramValue = param.getValue().text();
if( paramName != null && paramValue != null )
{
params.put( paramName, paramValue );
}
}
}
if( entry instanceof MasterDetailsContentNodeDef )
{
final MasterDetailsContentNodeDef def = (MasterDetailsContentNodeDef) entry;
final MasterDetailsContentNodePart node = new MasterDetailsContentNodePart();
node.init( this, this.modelElement, def, params );
node.initialize();
node.attach( this.childPartListener );
this.rawChildren.add( node );
}
else if( entry instanceof MasterDetailsContentNodeFactoryDef )
{
final NodeFactory factory = new NodeFactory( (MasterDetailsContentNodeFactoryDef) entry, params );
this.rawChildren.add( factory );
}
else
{
throw new IllegalStateException();
}
}
refreshNodes();
attach
(
new Listener()
{
@Override
public void handle( final Event event )
{
if( event instanceof PartVisibilityEvent || event instanceof NodeListEvent )
{
getContentTree().refreshSelection();
}
}
}
);
}
@Override
protected Function initVisibleWhenFunction()
{
return AndFunction.create
(
super.initVisibleWhenFunction(),
createVersionCompatibleFunction( this.modelElementProperty ),
(
this.nodeFactoryVisibleFunction != null ?
this.nodeFactoryVisibleFunction :
new Function()
{
@Override
public String name()
{
return "VisibleIfChildrenVisible";
}
@Override
public FunctionResult evaluate( final FunctionContext context )
{
return new FunctionResult( this, context )
{
@Override
protected void init()
{
final Listener listener = new FilteredListener<PartVisibilityEvent>()
{
@Override
protected void handleTypedEvent( final PartVisibilityEvent event )
{
refresh();
}
};
for( SapphirePart section : getSections() )
{
section.attach( listener );
}
for( Object entry : MasterDetailsContentNodePart.this.rawChildren )
{
if( entry instanceof MasterDetailsContentNodePart )
{
( (MasterDetailsContentNodePart) entry ).attach( listener );
}
else if( entry instanceof NodeFactory )
{
( (NodeFactory) entry ).attach( listener );
}
}
}
@Override
protected Object evaluate()
{
boolean visible = false;
for( SectionPart section : getSections() )
{
if( section.visible() )
{
visible = true;
break;
}
}
if( ! visible )
{
visible = ( getChildNodeFactoryProperties().size() > 0 );
}
if( ! visible )
{
for( Object entry : MasterDetailsContentNodePart.this.rawChildren )
{
if( entry instanceof MasterDetailsContentNodePart )
{
final MasterDetailsContentNodePart node = (MasterDetailsContentNodePart) entry;
if( node.visible() )
{
visible = true;
break;
}
}
}
}
return visible;
}
};
}
}
)
);
}
public MasterDetailsContentOutline getContentTree()
{
return this.contentTree;
}
public MasterDetailsContentNodePart getParentNode()
{
return this.parentNode;
}
public boolean isAncestorOf( final MasterDetailsContentNodePart node )
{
MasterDetailsContentNodePart n = node;
while( n != null )
{
if( n == this )
{
return true;
}
n = n.getParentNode();
}
return false;
}
@Override
public Element getLocalModelElement()
{
return this.modelElement;
}
public String getLabel()
{
String label = null;
if( this.labelFunctionResult != null )
{
label = (String) this.labelFunctionResult.value();
}
if( label == null )
{
label = "#null#";
}
else
{
label = label.trim();
final CapitalizationType capType = ( this.transformLabelCase ? CapitalizationType.TITLE_STYLE : CapitalizationType.NO_CAPS );
label = this.definition.adapt( LocalizationService.class ).transform( label, capType, false );
}
return label;
}
public ImageDescriptor getImage()
{
return this.imageManager.getImage();
}
public List<TextDecoration> decorations()
{
return this.decorations;
}
public boolean isExpanded()
{
return this.expanded;
}
public void setExpanded( final boolean expanded )
{
setExpanded( expanded, false );
}
public void setExpanded( final boolean expanded,
final boolean applyToChildren )
{
if( this.parentNode != null && ! this.parentNode.isExpanded() && expanded == true )
{
this.parentNode.setExpanded( true );
}
if( this.expanded != expanded )
{
if( ! expanded )
{
final MasterDetailsContentNodePart selection = getContentTree().getSelectedNode();
if( selection != null && isAncestorOf( selection ) )
{
select();
}
}
if( expanded )
{
this.expanded = expanded;
getContentTree().notifyOfNodeExpandedStateChange( this );
}
}
if( applyToChildren )
{
for( MasterDetailsContentNodePart child : nodes() )
{
if( ! child.nodes().visible().isEmpty() )
{
child.setExpanded( expanded, applyToChildren );
}
}
}
if( this.expanded != expanded )
{
if( ! expanded )
{
this.expanded = expanded;
getContentTree().notifyOfNodeExpandedStateChange( this );
}
}
}
public List<MasterDetailsContentNodePart> getExpandedNodes()
{
final List<MasterDetailsContentNodePart> result = new ArrayList<MasterDetailsContentNodePart>();
getExpandedNodes( result );
return result;
}
public void getExpandedNodes( final List<MasterDetailsContentNodePart> result )
{
if( isExpanded() )
{
result.add( this );
for( MasterDetailsContentNodePart child : nodes() )
{
child.getExpandedNodes( result );
}
}
}
public void select()
{
getContentTree().setSelectedNode( this );
}
public List<SectionPart> getSections()
{
return this.sections;
}
public List<PropertyDef> getChildNodeFactoryProperties()
{
final ArrayList<PropertyDef> properties = new ArrayList<PropertyDef>();
for( Object object : this.rawChildren )
{
if( object instanceof NodeFactory )
{
final NodeFactory factory = (NodeFactory) object;
if( factory.visible() )
{
properties.add( factory.property().definition() );
}
}
}
return properties;
}
public List<NodeFactory> factories()
{
final ListFactory<NodeFactory> factories = ListFactory.start();
for( Object entry : this.rawChildren )
{
if( entry instanceof NodeFactory )
{
factories.add( (NodeFactory) entry );
}
}
return factories.result();
}
public MasterDetailsContentNodeList nodes()
{
if( this.nodes == null )
{
this.nodes = new MasterDetailsContentNodeList( Collections.<MasterDetailsContentNodePart>emptyList() );
}
return this.nodes;
}
public MasterDetailsContentNodePart findNode( final String label )
{
for( MasterDetailsContentNodePart child : nodes() )
{
if( label.equalsIgnoreCase( child.getLabel() ) )
{
return child;
}
}
return null;
}
public MasterDetailsContentNodePart findNode( final Element element )
{
if( getModelElement() == element )
{
return this;
}
for( MasterDetailsContentNodePart child : nodes() )
{
final MasterDetailsContentNodePart res = child.findNode( element );
if( res != null )
{
return res;
}
}
return null;
}
private void refreshNodes()
{
final ListFactory<MasterDetailsContentNodePart> nodeListFactory = ListFactory.start();
for( Object entry : this.rawChildren )
{
if( entry instanceof MasterDetailsContentNodePart )
{
nodeListFactory.add( (MasterDetailsContentNodePart) entry );
}
else if( entry instanceof NodeFactory )
{
nodeListFactory.add( ( ((NodeFactory) entry) ).nodes() );
}
else
{
throw new IllegalStateException( entry.getClass().getName() );
}
}
final MasterDetailsContentNodeList nodes = new MasterDetailsContentNodeList( nodeListFactory.result() );
if( this.nodes == null )
{
this.nodes = nodes;
}
else if( ! this.nodes.equals( nodes ) )
{
this.nodes = nodes;
broadcast( new NodeListEvent( this ) );
}
refreshValidation();
}
public PropertiesViewContributionPart getPropertiesViewContribution()
{
if( this.propertiesViewContributionManager == null )
{
this.propertiesViewContributionManager = new PropertiesViewContributionManager( this, getLocalModelElement() );
}
return this.propertiesViewContributionManager.getPropertiesViewContribution();
}
@Override
public Set<String> getActionContexts()
{
return Collections.singleton( SapphireActionSystem.CONTEXT_EDITOR_PAGE_OUTLINE_NODE );
}
@Override
protected Status computeValidation()
{
final Status.CompositeStatusFactory validation = Status.factoryForComposite();
for( final SapphirePart section : this.sections )
{
if( section.visible() )
{
validation.merge( section.validation() );
}
}
for( final SapphirePart node : nodes() )
{
if( node.visible() )
{
validation.merge( node.validation() );
}
}
for( final NodeFactory factory : factories() )
{
if( factory.visible() )
{
validation.merge( factory.property().validation() );
}
}
return validation.create();
}
@Override
public void dispose()
{
super.dispose();
for( final SapphirePart section : this.sections )
{
section.dispose();
}
for( final SapphirePart node : nodes() )
{
node.dispose();
}
if( this.labelFunctionResult != null )
{
this.labelFunctionResult.dispose();
}
if( this.imageManager != null )
{
this.imageManager.dispose();
}
if( this.decorations != null )
{
for( final TextDecoration decoration : this.decorations )
{
decoration.dispose();
}
this.decorations = null;
}
for( final Object object : this.rawChildren )
{
if( object instanceof NodeFactory )
{
( (NodeFactory) object ).dispose();
}
}
}
public boolean controls( final Element element )
{
if( element == getModelElement() )
{
final ISapphirePart parentPart = parent();
if( parentPart != null && parentPart instanceof MasterDetailsContentNodePart )
{
final MasterDetailsContentNodePart parentNode = (MasterDetailsContentNodePart) parentPart;
return ( element != parentNode.getLocalModelElement() );
}
}
return false;
}
public final class NodeFactory
{
private final Property property;
private final Listener propertyListener;
private final MasterDetailsContentNodeFactoryDef definition;
private final Map<String,String> params;
private final FunctionResult visibleWhenFunctionResult;
private final Function visibleWhenFunctionForNodes;
private final Map<Element,MasterDetailsContentNodePart> nodesCache = new IdentityHashMap<Element,MasterDetailsContentNodePart>();
private final ListenerContext listeners = new ListenerContext();
public NodeFactory( final MasterDetailsContentNodeFactoryDef definition,
final Map<String,String> params )
{
final ModelPath path = new ModelPath( substituteParams( definition.getProperty().content(), params ) );
Element element = getLocalModelElement();
Property p = null;
for( int i = 0, n = path.length(); i < n; i++ )
{
if( p != null )
{
throw new RuntimeException( path.toString() );
}
final ModelPath.Segment segment = path.segment( i );
if( segment instanceof ModelPath.ModelRootSegment )
{
element = element.root();
}
else if( segment instanceof ModelPath.ParentElementSegment )
{
element = element.parent().element();
}
else if( segment instanceof ModelPath.PropertySegment )
{
final Property property = element.property( ( (ModelPath.PropertySegment) segment ).getPropertyName() );
if( property != null && property.definition() instanceof ImpliedElementProperty )
{
element = ( (ElementHandle<?>) property ).content();
}
else if( property instanceof ElementList || property instanceof ElementHandle )
{
p = property;
}
else
{
throw new RuntimeException( path.toString() );
}
}
else
{
throw new RuntimeException( path.toString() );
}
}
if( p == null )
{
throw new RuntimeException( path.toString() );
}
this.property = p;
this.propertyListener = new Listener()
{
@Override
public void handle( final Event event )
{
if( event instanceof PropertyContentEvent )
{
refreshNodes();
}
else if( event instanceof PropertyValidationEvent )
{
runOnDisplayThread
(
new Runnable()
{
public void run()
{
refreshValidation();
}
}
);
}
}
};
this.property.attach( this.propertyListener );
this.definition = definition;
this.params = params;
this.visibleWhenFunctionResult = initExpression
(
AndFunction.create
(
definition.getVisibleWhen().content(),
createVersionCompatibleFunction( this.property )
),
Boolean.class,
Literal.TRUE,
new Runnable()
{
public void run()
{
broadcast( new PartVisibilityEvent( null ) );
}
}
);
this.visibleWhenFunctionForNodes = new Function()
{
@Override
public String name()
{
return "NodeFactoryVisible";
}
@Override
public FunctionResult evaluate( final FunctionContext context )
{
return new FunctionResult( this, context )
{
private Listener listener;
@Override
protected void init()
{
this.listener = new Listener()
{
@Override
public void handle( final Event event )
{
refresh();
}
};
NodeFactory.this.visibleWhenFunctionResult.attach( this.listener );
}
@Override
protected Object evaluate()
{
return NodeFactory.this.visible();
}
@Override
public void dispose()
{
super.dispose();
NodeFactory.this.visibleWhenFunctionResult.detach( this.listener );
}
};
}
};
this.visibleWhenFunctionForNodes.init();
}
public final boolean visible()
{
return (Boolean) this.visibleWhenFunctionResult.value();
}
public Property property()
{
return this.property;
}
protected List<Element> elements()
{
final ListFactory<Element> elementsListFactory = ListFactory.start();
if( this.property instanceof ElementList )
{
for( Element element : (ElementList<?>) this.property )
{
elementsListFactory.add( element );
}
}
else
{
elementsListFactory.add( ( (ElementHandle<?>) this.property ).content() );
}
return elementsListFactory.result();
}
public final List<MasterDetailsContentNodePart> nodes()
{
final ListFactory<MasterDetailsContentNodePart> nodes = ListFactory.start();
for( final Iterator<Map.Entry<Element,MasterDetailsContentNodePart>> itr = this.nodesCache.entrySet().iterator(); itr.hasNext(); )
{
final Map.Entry<Element,MasterDetailsContentNodePart> entry = itr.next();
final Element element = entry.getKey();
final MasterDetailsContentNodePart node = entry.getValue();
if( element.disposed() )
{
node.dispose();
}
}
for( Element element : elements() )
{
MasterDetailsContentNodePart node = this.nodesCache.get( element );
if( node == null )
{
MasterDetailsContentNodeDef relevantCaseDef = null;
for( MasterDetailsContentNodeFactoryCaseDef entry : this.definition.getCases() )
{
final JavaType type = entry.getElementType().target();
if( type == null )
{
relevantCaseDef = entry;
break;
}
else
{
final Class<?> cl = (Class<?>) type.artifact();
if( cl == null || cl.isAssignableFrom( element.getClass() ) )
{
relevantCaseDef = entry;
break;
}
}
}
if( relevantCaseDef == null )
{
throw new RuntimeException();
}
node = new MasterDetailsContentNodePart( this.visibleWhenFunctionForNodes );
// It is very important to put the node into the cache prior to initializing the node as
// initialization can cause a re-entrant call into this function and we must avoid creating
// two nodes for the same element.
this.nodesCache.put( element, node );
node.init( MasterDetailsContentNodePart.this, element, relevantCaseDef, this.params );
node.initialize();
node.attach( MasterDetailsContentNodePart.this.childPartListener );
node.transformLabelCase = false;
}
nodes.add( node );
}
return nodes.result();
}
public final boolean attach( final Listener listener )
{
return this.listeners.attach( listener );
}
public final boolean detach( final Listener listener )
{
return this.listeners.detach( listener );
}
protected final void broadcast( final Event event )
{
this.listeners.broadcast( event );
}
public void dispose()
{
this.property.detach( this.propertyListener );
if( this.visibleWhenFunctionResult != null )
{
this.visibleWhenFunctionResult.dispose();
}
}
}
public static final class DecorationEvent extends PartEvent
{
private DecorationEvent( final MasterDetailsContentNodePart node )
{
super( node );
}
@Override
public MasterDetailsContentNodePart part()
{
return (MasterDetailsContentNodePart) super.part();
}
}
public static final class NodeListEvent extends PartEvent
{
private NodeListEvent( final MasterDetailsContentNodePart node )
{
super( node );
}
@Override
public MasterDetailsContentNodePart part()
{
return (MasterDetailsContentNodePart) super.part();
}
}
}