/*******************************************************************************
* Copyright (c) 2010, 2017 itemis AG and others.
*
* 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:
* Fabian Steeg - intial Xtext generation (see bug #277380)
* Alexander Nyßen - initial implementation
* Tamas Miklossy (itemis AG) - Add support for all dot attributes (bug #461506)
* - Improve the content assistant support (bug #498324)
*
*******************************************************************************/
package org.eclipse.gef.dot.internal.ui.language.contentassist;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.gef.dot.internal.DotAttributes;
import org.eclipse.gef.dot.internal.DotAttributes.Context;
import org.eclipse.gef.dot.internal.DotImport;
import org.eclipse.gef.dot.internal.language.clustermode.ClusterMode;
import org.eclipse.gef.dot.internal.language.color.DotColors;
import org.eclipse.gef.dot.internal.language.dir.DirType;
import org.eclipse.gef.dot.internal.language.dot.AttrList;
import org.eclipse.gef.dot.internal.language.dot.AttrStmt;
import org.eclipse.gef.dot.internal.language.dot.Attribute;
import org.eclipse.gef.dot.internal.language.dot.DotGraph;
import org.eclipse.gef.dot.internal.language.dot.EdgeOp;
import org.eclipse.gef.dot.internal.language.dot.EdgeStmtNode;
import org.eclipse.gef.dot.internal.language.dot.GraphType;
import org.eclipse.gef.dot.internal.language.dot.NodeStmt;
import org.eclipse.gef.dot.internal.language.layout.Layout;
import org.eclipse.gef.dot.internal.language.outputmode.OutputMode;
import org.eclipse.gef.dot.internal.language.pagedir.Pagedir;
import org.eclipse.gef.dot.internal.language.rankdir.Rankdir;
import org.eclipse.gef.dot.internal.language.services.DotGrammarAccess;
import org.eclipse.gef.dot.internal.language.splines.Splines;
import org.eclipse.gef.dot.internal.language.style.EdgeStyle;
import org.eclipse.gef.dot.internal.language.style.NodeStyle;
import org.eclipse.gef.dot.internal.language.terminals.ID;
import org.eclipse.gef.dot.internal.language.terminals.ID.Type;
import org.eclipse.gef.dot.internal.ui.language.internal.DotActivator;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.swt.graphics.Image;
import org.eclipse.xtext.Assignment;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.Keyword;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.ui.editor.contentassist.ConfigurableCompletionProposal;
import org.eclipse.xtext.ui.editor.contentassist.ContentAssistContext;
import org.eclipse.xtext.ui.editor.contentassist.ICompletionProposalAcceptor;
import com.google.inject.Inject;
/**
* A proposal provider for Dot.
*
* @author anyssen
*/
public class DotProposalProvider extends AbstractDotProposalProvider {
@Inject
DotGrammarAccess dotGrammarAccess;
private static Map<Context, List<String>> dotAttributeNames;
private String[] booleanAttributeValuesProposals = { "true", "false" }; //$NON-NLS-1$ //$NON-NLS-2$
public DotProposalProvider() {
// collect the dot attribute names on demand
if (dotAttributeNames == null) {
dotAttributeNames = getDotAttributeNames();
}
}
@Override
protected ICompletionProposal createCompletionProposal(String proposal,
StyledString displayString, Image image, int priority,
String prefix, ContentAssistContext context) {
if (context.getCurrentModel() instanceof DotGraph
&& ((DotGraph) context.getCurrentModel())
.getType() == GraphType.DIGRAPH
&& proposal.equals(EdgeOp.UNDIRECTED.toString())) {
// do not propose the undirected edge operator in case of a directed
// graph
return null;
}
if (context.getCurrentModel() instanceof DotGraph
&& ((DotGraph) context.getCurrentModel())
.getType() == GraphType.GRAPH
&& proposal.equals(EdgeOp.DIRECTED.toString())) {
// do not propose the directed edge operator in case of an
// undirected graph
return null;
}
if (context.getPrefix().equals("=") && proposal.equals("=")) { //$NON-NLS-1$//$NON-NLS-2$
// do not propose the "=" symbol if it is already included in the
// text as prefix
return null;
}
if (context.getPrefix().equals("[") && proposal.equals("[")) { //$NON-NLS-1$ //$NON-NLS-2$
// do not propose the "[" symbol if it is already included in the
// text as prefix
return null;
}
ICompletionProposal completionProposal = super.createCompletionProposal(
proposal, displayString, image, priority, prefix, context);
// ensure that the double quote at the beginning of an attribute value
// is not overridden when applying the proposal
if (completionProposal instanceof ConfigurableCompletionProposal
&& context.getCurrentModel() instanceof Attribute) {
INode currentNode = context.getCurrentNode();
String text = currentNode.getText();
if (text.startsWith("\"")) { //$NON-NLS-1$
ConfigurableCompletionProposal configurableCompletionProposal = (ConfigurableCompletionProposal) completionProposal;
configurableCompletionProposal.setReplacementOffset(
configurableCompletionProposal.getReplacementOffset()
+ 1);
configurableCompletionProposal.setReplacementLength(
configurableCompletionProposal.getReplacementLength()
- 1);
}
}
return completionProposal;
}
@Override
protected boolean isValidProposal(String proposal, String prefix,
ContentAssistContext context) {
// consider a double quote as a valid prefix for the attribute values
if (context.getCurrentModel() instanceof Attribute
&& prefix.startsWith("\"")) { //$NON-NLS-1$
return super.isValidProposal(proposal, prefix.substring(1),
context);
}
return super.isValidProposal(proposal, prefix, context);
}
@Override
public void completePort_Compass_pt(EObject model, Assignment assignment,
ContentAssistContext context,
ICompletionProposalAcceptor acceptor) {
for (Keyword k : EcoreUtil2.getAllContentsOfType(
dotGrammarAccess.getCOMPASS_PTRule().getAlternatives(),
Keyword.class)) {
acceptor.accept(createCompletionProposal(k.getValue(), context));
}
super.completePort_Compass_pt(model, assignment, context, acceptor);
}
@Override
public void completeAttribute_Name(EObject model, Assignment assignment,
ContentAssistContext contentAssistContext,
ICompletionProposalAcceptor acceptor) {
super.completeAttribute_Name(model, assignment, contentAssistContext,
acceptor);
if (model instanceof AttrList) {
Context attributeContext = DotAttributes.getContext(model);
proposeAttributeNames(attributeContext, contentAssistContext,
acceptor);
} else if (model instanceof DotGraph || model instanceof NodeStmt) {
proposeAttributeNames(Context.GRAPH, contentAssistContext,
acceptor);
}
}
@Override
public void completeAttribute_Value(EObject model, Assignment assignment,
ContentAssistContext context,
ICompletionProposalAcceptor acceptor) {
if (model instanceof Attribute) {
Attribute attribute = (Attribute) model;
if (DotAttributes.getContext(attribute) == Context.EDGE) {
switch (attribute.getName().toValue()) {
case DotAttributes.ARROWHEAD__E:
case DotAttributes.ARROWTAIL__E:
proposeAttributeValues(
DotActivator.ORG_ECLIPSE_GEF_DOT_INTERNAL_LANGUAGE_DOTARROWTYPE,
context, acceptor);
break;
case DotAttributes.COLOR__CNE:
case DotAttributes.FILLCOLOR__CNE:
case DotAttributes.FONTCOLOR__GCNE:
case DotAttributes.LABELFONTCOLOR__E:
proposeColorAttributeValues(attribute, context, acceptor);
break;
case DotAttributes.COLORSCHEME__GCNE:
proposeAttributeValues(DotColors.getColorSchemes(), context,
acceptor);
break;
case DotAttributes.DIR__E:
proposeAttributeValues(DirType.values(), context, acceptor);
break;
case DotAttributes.HEAD_LP__E:
case DotAttributes.LP__GCE:
case DotAttributes.TAIL_LP__E:
case DotAttributes.XLP__NE:
proposeAttributeValues(
DotActivator.ORG_ECLIPSE_GEF_DOT_INTERNAL_LANGUAGE_DOTPOINT,
context, acceptor);
break;
case DotAttributes.HEADLABEL__E:
case DotAttributes.LABEL__GCNE:
case DotAttributes.TAILLABEL__E:
case DotAttributes.XLABEL__NE:
proposeHtmlLabelAttributeValues(attribute, context,
acceptor);
break;
case DotAttributes.POS__NE:
proposeAttributeValues(
DotActivator.ORG_ECLIPSE_GEF_DOT_INTERNAL_LANGUAGE_DOTSPLINETYPE,
context, acceptor);
break;
case DotAttributes.STYLE__GCNE:
proposeAttributeValues(EdgeStyle.VALUES, context, acceptor);
break;
default:
super.completeAttribute_Value(model, assignment, context,
acceptor);
break;
}
} else if (DotAttributes.getContext(attribute) == Context.GRAPH) {
switch (attribute.getName().toValue()) {
case DotAttributes.BGCOLOR__GC:
case DotAttributes.FONTCOLOR__GCNE:
proposeColorAttributeValues(attribute, context, acceptor);
break;
case DotAttributes.CLUSTERRANK__G:
proposeAttributeValues(ClusterMode.values(), context,
acceptor);
break;
case DotAttributes.COLORSCHEME__GCNE:
proposeAttributeValues(DotColors.getColorSchemes(), context,
acceptor);
break;
case DotAttributes.FORCELABELS__G:
proposeAttributeValues(booleanAttributeValuesProposals,
context, acceptor);
break;
case DotAttributes.LABEL__GCNE:
proposeHtmlLabelAttributeValues(attribute, context,
acceptor);
break;
case DotAttributes.LAYOUT__G:
proposeAttributeValues(Layout.values(), context, acceptor);
break;
case DotAttributes.LP__GCE:
proposeAttributeValues(
DotActivator.ORG_ECLIPSE_GEF_DOT_INTERNAL_LANGUAGE_DOTPOINT,
context, acceptor);
break;
case DotAttributes.OUTPUTORDER__G:
proposeAttributeValues(OutputMode.values(), context,
acceptor);
break;
case DotAttributes.PAGEDIR__G:
proposeAttributeValues(Pagedir.values(), context, acceptor);
break;
case DotAttributes.RANKDIR__G:
proposeAttributeValues(Rankdir.values(), context, acceptor);
break;
case DotAttributes.SPLINES__G:
proposeAttributeValues(Splines.values(), context, acceptor);
break;
default:
super.completeAttribute_Value(model, assignment, context,
acceptor);
break;
}
} else if (DotAttributes.getContext(attribute) == Context.NODE) {
switch (attribute.getName().toValue()) {
case DotAttributes.COLOR__CNE:
case DotAttributes.FILLCOLOR__CNE:
case DotAttributes.FONTCOLOR__GCNE:
proposeColorAttributeValues(attribute, context, acceptor);
break;
case DotAttributes.COLORSCHEME__GCNE:
proposeAttributeValues(DotColors.getColorSchemes(), context,
acceptor);
break;
case DotAttributes.FIXEDSIZE__N:
proposeAttributeValues(booleanAttributeValuesProposals,
context, acceptor);
break;
case DotAttributes.LABEL__GCNE:
case DotAttributes.XLABEL__NE:
proposeHtmlLabelAttributeValues(attribute, context,
acceptor);
break;
case DotAttributes.POS__NE:
case DotAttributes.XLP__NE:
proposeAttributeValues(
DotActivator.ORG_ECLIPSE_GEF_DOT_INTERNAL_LANGUAGE_DOTPOINT,
context, acceptor);
break;
case DotAttributes.SHAPE__N:
proposeAttributeValues(
DotActivator.ORG_ECLIPSE_GEF_DOT_INTERNAL_LANGUAGE_DOTSHAPE,
context, acceptor);
break;
case DotAttributes.STYLE__GCNE:
proposeAttributeValues(NodeStyle.VALUES, context, acceptor);
break;
default:
break;
}
} else if (DotAttributes.getContext(attribute) == Context.CLUSTER
|| DotAttributes
.getContext(attribute) == Context.SUBGRAPH) {
switch (attribute.getName().toValue()) {
case DotAttributes.LABEL__GCNE:
proposeHtmlLabelAttributeValues(attribute, context,
acceptor);
break;
}
}
else {
super.completeAttribute_Value(model, assignment, context,
acceptor);
}
} else {
super.completeAttribute_Value(model, assignment, context, acceptor);
}
}
private void proposeAttributeNames(Context attributeContext,
ContentAssistContext contentAssistContext,
ICompletionProposalAcceptor acceptor) {
for (String attributeName : dotAttributeNames.get(attributeContext)) {
ICompletionProposal completionProposal = createCompletionProposal(
attributeName, contentAssistContext);
acceptor.accept(completionProposal);
}
}
private void proposeAttributeValues(String subgrammarName,
ContentAssistContext context,
ICompletionProposalAcceptor acceptor) {
String text = context.getPrefix();
if (text.startsWith("\"")) { //$NON-NLS-1$
text = ID.fromString(text, Type.QUOTED_STRING).toValue();
context = context.copy().setPrefix(text).toContext();
}
if (text.startsWith("<")) { //$NON-NLS-1$
text = ID.fromString(text, Type.HTML_STRING).toValue();
context = context.copy().setPrefix(text).toContext();
}
List<ConfigurableCompletionProposal> configurableCompletionProposals = new DotProposalProviderDelegator(
subgrammarName).computeConfigurableCompletionProposals(text,
text.length());
for (ConfigurableCompletionProposal configurableCompletionProposal : configurableCompletionProposals) {
// adapt the replacement offset determined within the
// sub-grammar context to be valid within the context of the
// original text
configurableCompletionProposal.setReplacementOffset(
context.getOffset() - configurableCompletionProposal
.getReplaceContextLength());
acceptor.accept(configurableCompletionProposal);
}
}
private void proposeAttributeValues(Object[] values,
ContentAssistContext context,
ICompletionProposalAcceptor acceptor) {
proposeAttributeValues(Arrays.asList(values), context, acceptor);
}
private void proposeAttributeValues(List<?> values,
ContentAssistContext context,
ICompletionProposalAcceptor acceptor) {
for (Object value : values) {
final String proposedValue = ID.fromValue(value.toString())
.toString();
acceptor.accept(createCompletionProposal(proposedValue, context));
}
}
// TODO: add color constants to delegate color proposal provider
private void proposeColorAttributeValues(Attribute attribute,
ContentAssistContext context,
ICompletionProposalAcceptor acceptor) {
// propose color values based on the DotColor sub-grammar
proposeAttributeValues(
DotActivator.ORG_ECLIPSE_GEF_DOT_INTERNAL_LANGUAGE_DOTCOLOR,
context, acceptor);
// propose color values based on the current color scheme
List<String> validColorNames = getColorNames(attribute);
proposeAttributeValues(validColorNames, context, acceptor);
}
private void proposeHtmlLabelAttributeValues(Attribute attribute,
ContentAssistContext context,
ICompletionProposalAcceptor acceptor) {
if (attribute.getValue() != null
&& attribute.getValue().getType() == Type.HTML_STRING) {
proposeAttributeValues(
DotActivator.ORG_ECLIPSE_GEF_DOT_INTERNAL_LANGUAGE_DOTHTMLLABEL,
context, acceptor);
}
}
/**
* Calculates the valid dot attribute names within a given {@link Context}.
*
* @return a map mapping the {@link Context} elements such as
* {@link Context#EDGE}, {@link Context#GRAPH}, {@link Context#NODE}
* to the valid dot attribute names.
*/
private Map<Context, List<String>> getDotAttributeNames() {
List<String> edgeAttributeNames = new ArrayList<>();
List<String> graphAttributeNames = new ArrayList<>();
List<String> nodeAttributeNames = new ArrayList<>();
Field[] declaredFields = DotAttributes.class.getDeclaredFields();
for (Field field : declaredFields) {
int modifiers = field.getModifiers();
if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers)
&& Modifier.isFinal(modifiers)
&& field.getType().equals(String.class)) {
String fieldName = field.getName();
String dotClassifier = fieldName
.substring(fieldName.lastIndexOf("_") + 1); //$NON-NLS-1$
if (!fieldName.startsWith("_")) { //$NON-NLS-1$
String dotAttributeName = null;
try {
dotAttributeName = (String) field.get(null);
} catch (IllegalArgumentException
| IllegalAccessException e) {
e.printStackTrace();
}
if (dotAttributeName != null) {
if (dotClassifier.contains("E")) { //$NON-NLS-1$
edgeAttributeNames.add(dotAttributeName);
}
if (dotClassifier.contains("G")) { //$NON-NLS-1$
graphAttributeNames.add(dotAttributeName);
}
if (dotClassifier.contains("N")) { //$NON-NLS-1$
nodeAttributeNames.add(dotAttributeName);
}
}
}
}
}
Map<Context, List<String>> dotAttributeNames = new HashMap<>();
dotAttributeNames.put(Context.EDGE, edgeAttributeNames);
dotAttributeNames.put(Context.GRAPH, graphAttributeNames);
dotAttributeNames.put(Context.NODE, nodeAttributeNames);
return dotAttributeNames;
}
private List<String> getColorNames(Attribute attribute) {
// attribute nested below EdgeStmtNode
EObject container = EcoreUtil2.getContainerOfType(attribute,
EdgeStmtNode.class);
if (container != null) {
ID colorScheme = DotImport.getAttributeValue(
((EdgeStmtNode) container).getAttrLists(),
DotAttributes.COLORSCHEME__GCNE);
if (colorScheme != null) {
return DotColors.getColorNames(colorScheme.toValue());
}
}
// attribute nested below NodeStmt
container = EcoreUtil2.getContainerOfType(attribute, NodeStmt.class);
if (container != null) {
ID colorScheme = DotImport.getAttributeValue(
((NodeStmt) container).getAttrLists(),
DotAttributes.COLORSCHEME__GCNE);
if (colorScheme != null) {
return DotColors.getColorNames(colorScheme.toValue());
}
}
// attribute nested below Graph
container = EcoreUtil2.getContainerOfType(attribute, DotGraph.class);
if (container != null) {
ID colorScheme = DotImport.getAttributeValue((DotGraph) container,
DotAttributes.COLORSCHEME__GCNE);
if (colorScheme != null) {
return DotColors.getColorNames(colorScheme.toValue());
}
}
// global AttrStmt
AttrStmt attrStmt = EcoreUtil2.getContainerOfType(attribute,
AttrStmt.class);
if (attrStmt != null) {
ID colorScheme = DotImport.getAttributeValue(
attrStmt.getAttrLists(), DotAttributes.COLORSCHEME__GCNE);
if (colorScheme != null) {
return DotColors.getColorNames(colorScheme.toValue());
}
}
// return the default color scheme
return DotColors.getColorNames("x11"); //$NON-NLS-1$
}
}