package org.angularjs.codeInsight.router;
import com.intellij.diagram.*;
import com.intellij.diagram.components.DiagramNodeContainer;
import com.intellij.diagram.extras.DiagramExtras;
import com.intellij.diagram.presentation.DiagramState;
import com.intellij.icons.AllIcons;
import com.intellij.lang.javascript.modules.diagramm.JSModulesDiagramUtils;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.graph.GraphManager;
import com.intellij.openapi.graph.GraphUtil;
import com.intellij.openapi.graph.base.Edge;
import com.intellij.openapi.graph.builder.util.GraphViewUtil;
import com.intellij.openapi.graph.geom.YPoint;
import com.intellij.openapi.graph.layout.CanonicMultiStageLayouter;
import com.intellij.openapi.graph.layout.Layouter;
import com.intellij.openapi.graph.layout.ParallelEdgeLayouter;
import com.intellij.openapi.graph.layout.organic.SmartOrganicLayouter;
import com.intellij.openapi.graph.settings.GraphSettings;
import com.intellij.openapi.graph.settings.GraphSettingsProvider;
import com.intellij.openapi.graph.view.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.SmartPsiElementPointer;
import com.intellij.ui.*;
import com.intellij.uml.UmlGraphBuilder;
import com.intellij.uml.core.renderers.DefaultUmlRenderer;
import com.intellij.uml.presentation.DiagramPresentationModelImpl;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ui.JBUI;
import org.intellij.lang.annotations.Pattern;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.StrokeBorder;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Irina.Chernushina on 3/23/2016.
*/
public class AngularUiRouterDiagramProvider extends BaseDiagramProvider<DiagramObject> {
public static final String ANGULAR_UI_ROUTER = "Angular-ui-router";
public static final JBColor VIEW_COLOR = new JBColor(new Color(0xE1FFFC), new Color(0x589df6));
public static final BasicStroke DOTTED_STROKE =
new BasicStroke(0.7f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2, 2}, 0.0f);
public static final StrokeBorder WARNING_BORDER = new StrokeBorder(DOTTED_STROKE, JBColor.red);
public static final Border ERROR_BORDER = JBUI.Borders.customLine(JBColor.red);
public static final Border NORMAL_BORDER = JBUI.Borders.customLine(Gray._190);
private DiagramVfsResolver<DiagramObject> myResolver;
private AbstractDiagramElementManager<DiagramObject> myElementManager;
private DiagramColorManagerBase myColorManager;
public AngularUiRouterDiagramProvider() {
myResolver = new DiagramVfsResolver<DiagramObject>() {
@Override
public String getQualifiedName(DiagramObject element) {
if ((Type.template.equals(element.getType()) || Type.topLevelTemplate.equals(element.getType())) &&
element.getNavigationTarget() != null) {
final PsiFile psiFile = element.getNavigationTarget().getContainingFile();
return psiFile == null ? "" : psiFile.getVirtualFile().getPath();
}
else {
return "";
}
}
@Nullable
@Override
public DiagramObject resolveElementByFQN(String fqn, Project project) {
final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(fqn);
if (file == null) {
return null;
}
else {
final AngularUiRouterGraphBuilder.GraphNodesBuilder builder = AngularUiRouterProviderContext.getInstance(project).getBuilder(file);
return builder == null ? null : builder.getRootNode().getIdentifyingElement();
}
}
};
myElementManager = new AbstractDiagramElementManager<DiagramObject>() {
@Override
public Object[] getNodeItems(DiagramObject parent) {
return ArrayUtil.toObjectArray(parent.getChildrenList());
}
@Nullable
@Override
public DiagramObject findInDataContext(DataContext context) {
//todo ?
return null;
}
@Override
public boolean isAcceptableAsNode(Object element) {
return element instanceof DiagramObject;
}
@Nullable
@Override
public String getElementTitle(DiagramObject element) {
return element.getName();
}
@Nullable
@Override
public SimpleColoredText getItemName(Object element, DiagramState presentation) {
if (element instanceof DiagramObject) {
return new SimpleColoredText(((DiagramObject)element).getName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
return null;
}
@Override
public String getNodeTooltip(DiagramObject element) {
final List<String> errors = element.getErrors();
final List<String> warnings = element.getWarnings();
final List<String> notes = element.getNotes();
if (errors.isEmpty() && warnings.isEmpty() && notes.isEmpty()) return element.getTooltip();
final StringBuilder sb = new StringBuilder(element.getTooltip());
if (!notes.isEmpty()) {
for (String note : notes) {
sb.append('\n').append(note);
}
}
sb.append("<font style=\"color:#ff0000;\">");
if (!errors.isEmpty()) {
sb.append('\n').append(StringUtil.pluralize("Error", errors.size())).append(":\n");
for (String error : errors) {
sb.append(error).append('\n');
}
}
if (!warnings.isEmpty()) {
sb.append('\n').append(StringUtil.pluralize("Warning", warnings.size())).append(":\n");
for (String warning : warnings) {
sb.append(warning).append('\n');
}
}
sb.append("</font>");
return sb.toString();
}
@Override
public Icon getItemIcon(Object element, DiagramState presentation) {
return null; //do not show icons
}
};
myColorManager = new DiagramColorManagerBase() {
@Override
public Color getNodeHeaderColor(DiagramBuilder builder, @Nullable DiagramNode node) {
return getColor(node.getIdentifyingElement());
}
@Override
public Color getNodeBackground(Project project, Object nodeElement, boolean selected) {
return getColor(nodeElement);
}
@Nullable
private Color getColor(Object nodeElement) {
if (nodeElement instanceof DiagramObject) {
final DiagramObject element = ((DiagramObject)nodeElement);
if (Type.state.equals(element.getType())) {
return LightColors.YELLOW;
}
else if (Type.view.equals(element.getType())) {
return VIEW_COLOR;
}
else if (Type.template.equals(element.getType())) {
return LightColors.GREEN;
}
else if (Type.templatePlaceholder.equals(element.getType())) {
return LightColors.SLIGHTLY_GREEN;
}
}
return null;
}
@Override
public boolean drawGradientInHeader() {
return false;
}
};
}
@Override
public DiagramColorManager getColorManager() {
return myColorManager;
}
@Pattern("[a-zA-Z0-9_-]*")
@Override
public String getID() {
return ANGULAR_UI_ROUTER;
}
@Override
public DiagramElementManager<DiagramObject> getElementManager() {
return myElementManager;
}
@Override
public DiagramVfsResolver<DiagramObject> getVfsResolver() {
return myResolver;
}
@Override
public String getPresentableName() {
return "AngularJS ui-router states and views";
}
@Override
public DiagramDataModel<DiagramObject> createDataModel(@NotNull Project project,
@Nullable DiagramObject element,
@Nullable VirtualFile file,
DiagramPresentationModel presentationModel) {
if (element == null || element.getNavigationTarget() == null) return null;
final VirtualFile virtualFile = element.getNavigationTarget().getContainingFile().getVirtualFile();
final AngularUiRouterGraphBuilder.GraphNodesBuilder nodesBuilder = AngularUiRouterProviderContext.getInstance(project).getBuilder(virtualFile);
if (nodesBuilder == null) return new AngularUiRouterDiagramModel(project, virtualFile, this, Collections.emptyList(), Collections.emptyList());
return new AngularUiRouterDiagramModel(project, virtualFile, this, nodesBuilder.getAllNodes(), nodesBuilder.getEdges());
}
@Nullable
@Override
public DiagramPresentationModel createPresentationModel(Project project, Graph2D graph) {
return new DiagramPresentationModelImpl(graph, project, this) {
@Override
public boolean allowChangeVisibleCategories() {
return false;
}
private final Map<DiagramEdge, EdgeRealizer> myEdgeRealizers = new HashMap<>();
@NotNull
@Override
public EdgeRealizer getEdgeRealizer(DiagramEdge edge) {
if (!(edge instanceof AngularUiRouterEdge)) return super.getEdgeRealizer(edge);
if (myEdgeRealizers.containsKey(edge)) return myEdgeRealizers.get(edge);
UmlGraphBuilder builder = (UmlGraphBuilder)graph.getDataProvider(DiagramDataKeys.GRAPH_BUILDER).get(null);
final Edge graphEdge = builder.getEdge(edge);
final AngularEdgeLayouter.OneEdgeLayouter layouter =
new AngularEdgeLayouter.OneEdgeLayouter(graphEdge, (AngularUiRouterEdge)edge, graph);
layouter.calculateEdgeLayout();
final QuadCurveEdgeRealizer realizer = layouter.getRealizer();
for (int i = 0; i < realizer.labelCount(); i++) {
realizer.removeLabel(realizer.getLabel(i));
}
/*final EdgeLabel[] labels = getEdgeLabels(edge, "");
if (labels.length == 0) realizer.setLabelText("");
else {
for (EdgeLabel label : labels) {
realizer.addLabel(label);
}
}*/
myEdgeRealizers.put(edge, realizer);
return realizer;
}
private Map<Integer, Integer> myEdgesPositions = new HashMap<>();
private final Set<AngularUiRouterEdge> myVisibleEdges = new HashSet<>();
@Override
public EdgeLabel[] getEdgeLabels(DiagramEdge umlEdge, String label) {
if (!(umlEdge instanceof AngularUiRouterEdge)) return super.getEdgeLabels(umlEdge, label);
AngularUiRouterEdge angularEdge = (AngularUiRouterEdge)umlEdge;
if ( !isShowEdgeLabels() || umlEdge == null || StringUtil.isEmptyOrSpaces(angularEdge.getLabel())) {
return EMPTY_LABELS;
}
//if (!myVisibleEdges.contains(umlEdge)) return EMPTY_LABELS;
UmlGraphBuilder builder = (UmlGraphBuilder)graph.getDataProvider(DiagramDataKeys.GRAPH_BUILDER).get(null);
final Edge edge = builder.getEdge(umlEdge);
final EdgeRealizer edgeRealizer = getEdgeRealizer(umlEdge);
for (int i = 0; i < edgeRealizer.labelCount(); i++) {
edgeRealizer.removeLabel(edgeRealizer.getLabel(i));
}
final Integer position = calculatePosition(edge, builder);
final EdgeLabel edgeLabel = GraphManager.getGraphManager().createEdgeLabel();
final SmartEdgeLabelModel model = GraphManager.getGraphManager().createSmartEdgeLabelModel();
edgeLabel.setLabelModel(model, model.createDiscreteModelParameter(position));
edgeLabel.setFontSize(9);
edgeLabel.setDistance(5);
edgeLabel.setTextColor(JBColor.foreground());
myEdgesPositions.put(edge.index(), 1);
return new EdgeLabel[]{edgeLabel};
}
private Integer calculatePosition(final Edge edge, UmlGraphBuilder builder) {
final Integer existing = myEdgesPositions.get(edge.index());
if (existing != null) return existing;
final List<Edge> list = new ArrayList<>();
for (Edge current : edge.getGraph().getEdgeArray()) {
if (current.source().index() == edge.source().index() && current.target().index() == edge.target().index() ||
current.target().index() == edge.source().index() && current.source().index() == edge.target().index()) {
list.add(current);
}
}
boolean sourceHeavier = edge.source().degree() > edge.target().degree();
Collections.sort(list, (o1, o2) -> {
final YPoint s1 = ((Graph2D)o1.getGraph()).getSourcePointAbs(o1);
final YPoint s2 = ((Graph2D)o1.getGraph()).getSourcePointAbs(o2);
if (Math.abs(s1.getX() - s2.getX()) > 5) return Double.compare(s1.getX(), s2.getX());
return Double.compare(s1.getY(), s2.getY());
});
int[] variants = sourceHeavier ? new int[]{SmartEdgeLabelModel.POSITION_TARGET_RIGHT,
SmartEdgeLabelModel.POSITION_RIGHT, SmartEdgeLabelModel.POSITION_SOURCE_RIGHT} :
new int[]{SmartEdgeLabelModel.POSITION_SOURCE_RIGHT,
SmartEdgeLabelModel.POSITION_RIGHT, SmartEdgeLabelModel.POSITION_TARGET_RIGHT};
int variantIdx = 0;
for (Edge current : list) {
myEdgesPositions.put(current.index(), variants[variantIdx++]);
if (variantIdx >= variants.length) variantIdx = 0;
}
return myEdgesPositions.get(edge.index());
}
private boolean inUpdate = false;
@Override
public void update() {
if (inUpdate) return;
try {
inUpdate = true;
myEdgeRealizers.clear();
final List<DiagramNode> nodes = GraphUtil.getSelectedNodes(getGraphBuilder());
super.update();
myEdgesPositions.clear();
final DiagramBuilder builder = getBuilder();
builder.relayout();
builder.getView().fitContent();
builder.updateView();
if (!nodes.isEmpty()) {
final Collection<DiagramNode> objects = builder.getNodeObjects();
for (DiagramNode object : objects) {
if (isInSelectedNodes(nodes, object)) {
builder.getGraph().setSelected(builder.getNode(object), true);
}
}
}
updateBySelection(nodes.isEmpty() ? null : nodes.get(0));
} finally {
inUpdate = false;
}
}
@Override
public void customizeSettings(Graph2DView view, EditMode editMode) {
super.customizeSettings(view, editMode);
view.getGraph2D().addGraph2DSelectionListener(new Graph2DSelectionListener() {
@Override
public void onGraph2DSelectionEvent(Graph2DSelectionEvent _e) {
myEdgesPositions.clear();
updateBySelection(null);
view.updateView();
}
});
view.getJComponent().addComponentListener(new ComponentAdapter() {
@Override
public void componentShown(ComponentEvent e) {
ApplicationManager.getApplication().invokeLater(() -> {
UmlGraphBuilder builder = (UmlGraphBuilder)graph.getDataProvider(DiagramDataKeys.GRAPH_BUILDER).get(null);
builder.getPresentationModel().update();
});
}
});
ApplicationManager.getApplication().invokeLater(() -> {
UmlGraphBuilder builder = (UmlGraphBuilder)graph.getDataProvider(DiagramDataKeys.GRAPH_BUILDER).get(null);
final AngularUiRouterDiagramModel model = (AngularUiRouterDiagramModel)builder.getDataModel();
final AngularUiRouterNode rootNode = findDataObject(project, model).getRootNode();
updateBySelection(rootNode);
});
}
private void updateBySelection(DiagramNode node) {
myVisibleEdges.clear();
UmlGraphBuilder builder = (UmlGraphBuilder)graph.getDataProvider(DiagramDataKeys.GRAPH_BUILDER).get(null);
final List<DiagramNode> nodes = new ArrayList<>(GraphUtil.getSelectedNodes(builder));
if (node != null && !nodes.contains(node)) nodes.add(node);
DiagramNode selected = null;
for (DiagramEdge edge : builder.getEdgeObjects()) {
if (nodes.contains(edge.getSource())) {
selected = edge.getSource();
} else if (nodes.contains(edge.getTarget())) {
selected = edge.getTarget();
} else continue;
break;
}
if (selected == null) {
for (DiagramEdge edge : builder.getEdgeObjects()) {
if (isInSelectedNodes(nodes, edge.getSource())) {
selected = edge.getSource();
} else if (isInSelectedNodes(nodes, edge.getTarget())) {
selected = edge.getTarget();
} else continue;
break;
}
}
for (DiagramEdge edge : builder.getEdgeObjects()) {
if (!(edge instanceof AngularUiRouterEdge)) continue;
if (isShowEdgeLabels() && selected != null && (selected.equals(edge.getSource()) || selected.equals(edge.getTarget()))) {
myVisibleEdges.add((AngularUiRouterEdge)edge);
graph.setLabelText(builder.getEdge(edge), ((AngularUiRouterEdge) edge).getLabel());
} else {
graph.setLabelText(builder.getEdge(edge), "");
}
}
}
@Override
public DefaultUmlRenderer getRenderer() {
if (myRenderer == null) {
myRenderer = new DefaultUmlRenderer(getBuilder(), createModificationTracker()) {
@Override
public void tuneNode(NodeRealizer realizer, JPanel wrapper) {
wrapper.setBorder(JBUI.Borders.empty());
if (wrapper.getParent() instanceof JComponent) ((JComponent)wrapper.getParent()).setBorder(JBUI.Borders.empty());
super.tuneNode(realizer, wrapper);
}
};
}
return myRenderer;
}
};
}
private static boolean isInSelectedNodes(List<DiagramNode> nodes, DiagramNode node) {
for (DiagramNode diagramNode : nodes) {
if (!(node instanceof AngularUiRouterNode && diagramNode instanceof AngularUiRouterNode)) continue;
final DiagramObject selected = (DiagramObject)diagramNode.getIdentifyingElement();
final DiagramObject object = (DiagramObject)node.getIdentifyingElement();
if (selected.getType().equals(object.getType()) && selected.getName().equals(object.getName()) &&
selected.getNavigationTarget() != null && object.getNavigationTarget() != null &&
selected.getNavigationTarget().getVirtualFile().equals(object.getNavigationTarget().getVirtualFile())) return true;
}
return false;
}
@NotNull
@Override
public DiagramExtras<DiagramObject> getExtras() {
return new DiagramExtras<DiagramObject>() {
@Override
public List<AnAction> getExtraActions() {
return Collections.singletonList(new MyEditSourceAction());
}
@Nullable
@Override
public Object getData(String dataId, List<DiagramNode<DiagramObject>> list, DiagramBuilder builder) {
if (CommonDataKeys.PSI_ELEMENT.is(dataId) && list.size() == 1) {
final SmartPsiElementPointer target = list.get(0).getIdentifyingElement().getNavigationTarget();
return target == null ? null : target.getElement();
} else if (JSModulesDiagramUtils.DIAGRAM_BUILDER.is(dataId)) {
return builder;
}
return null;
}
@Nullable
@Override
public Layouter getCustomLayouter(Graph2D graph, Project project) {
final GraphSettingsProvider settingsProvider = GraphSettingsProvider.getInstance(project);
final GraphSettings settings = settingsProvider.getSettings(graph);
final SmartOrganicLayouter layouter = settings.getOrganicLayouter();
layouter.setNodeEdgeOverlapAvoided(true);
layouter.setNodeSizeAware(true);
layouter.setMinimalNodeDistance(60);
layouter.setNodeOverlapsAllowed(false);
layouter.setSmartComponentLayoutEnabled(true);
layouter.setConsiderNodeLabelsEnabled(true);
layouter.setDeterministic(true);
final List<CanonicMultiStageLayouter> list = new ArrayList<>();
list.add(layouter);
list.add(settings.getBalloonLayouter());
list.add(settings.getCircularLayouter());
list.add(settings.getDirectedOrthogonalLayouter());
//list.add(settings.getGroupLayouter());
list.add(settings.getHVTreeLayouter());
list.add(settings.getOrthogonalLayouter());
for (CanonicMultiStageLayouter current : list) {
final ParallelEdgeLayouter parallelEdgeLayouter = GraphManager.getGraphManager().createParallelEdgeLayouter();
parallelEdgeLayouter.setLineDistance(40);
parallelEdgeLayouter.setUsingAdaptiveLineDistances(false);
current.appendStage(parallelEdgeLayouter);
current.setParallelEdgeLayouterEnabled(false);
}
return layouter;
}
@NotNull
@Override
public JComponent createNodeComponent(DiagramNode<DiagramObject> node, DiagramBuilder builder, Point basePoint, JPanel wrapper) {
final DiagramNodeContainer container = new DiagramNodeContainer(node, builder, basePoint);
if (!GraphViewUtil.isPrintMode()) {
if (!node.getIdentifyingElement().getErrors().isEmpty()) {
container.setBorder(ERROR_BORDER);
} else if (!node.getIdentifyingElement().getWarnings().isEmpty()) {
container.setBorder(WARNING_BORDER);
} else {
container.setBorder(NORMAL_BORDER);
}
}
if (!node.getIdentifyingElement().getChildrenList().isEmpty()) container.getHeader().setBorder(JBUI.Borders.customLine(Gray._190, 0, 0, 1, 0));
return container;
}
};
}
private static AngularUiRouterGraphBuilder.GraphNodesBuilder findDataObject(Project project, final AngularUiRouterDiagramModel model) {
final Collection<AngularUiRouterNode> nodes = model.getNodes();
for (AngularUiRouterNode node : nodes) {
if (Type.topLevelTemplate.equals(node.getIdentifyingElement().getType())) {
final VirtualFile rootFile = node.getIdentifyingElement().getNavigationTarget().getContainingFile().getVirtualFile();
return AngularUiRouterProviderContext.getInstance(project).getBuilder(rootFile);
}
}
return null;
}
private static class MyEditSourceAction extends AnAction {
private final AnAction myAction;
public MyEditSourceAction() {
super("Jump To...", "Jump To...", AllIcons.Actions.EditSource);
myAction = ActionManager.getInstance().getAction(IdeActions.ACTION_EDIT_SOURCE);
}
@Override
public void update(AnActionEvent e) {
final Project project = CommonDataKeys.PROJECT.getData(e.getDataContext());
if (project == null) {
e.getPresentation().setEnabled(false);
return;
}
final List<DiagramNode> nodes = JSModulesDiagramUtils.getSelectedNodes(e);
e.getPresentation().setEnabled(nodes != null && nodes.size() == 1 && nodes.get(0) instanceof AngularUiRouterNode);
}
@Override
public void actionPerformed(AnActionEvent e) {
final Project project = CommonDataKeys.PROJECT.getData(e.getDataContext());
if (project == null) return;
final List<DiagramNode> nodes = JSModulesDiagramUtils.getSelectedNodes(e);
if (nodes == null || nodes.size() != 1 || !(nodes.get(0) instanceof AngularUiRouterNode)) return;
final AngularUiRouterNode node = (AngularUiRouterNode)nodes.get(0);
final DiagramObject main = node.getIdentifyingElement();
final List<DiagramObject> childrenList = main.getChildrenList();
if (childrenList.isEmpty()) myAction.actionPerformed(e);
else {
final List<Trinity<String, SmartPsiElementPointer, Icon>> children = childrenList.stream()
.map(ch -> Trinity.create(ch.getType().name() + ": " + ch.getName(), ch.getNavigationTarget(), (Icon)null))
.collect(Collectors.toList());
JSModulesDiagramUtils
.showMembersSelectionPopup(main.getType().name() + ": " + main.getName(), main.getNavigationTarget(), null, children, e.getDataContext());
}
}
}
}