/*******************************************************************************
* Copyright (c) 2013 Bundlemaker project team.
* 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:
* Bundlemaker project team - initial API and implementation
******************************************************************************/
package org.bundlemaker.core.ui.editor.dependencyviewer.graph;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Vector;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JPanel;
import org.bundlemaker.core.analysis.IBundleMakerArtifact;
import org.bundlemaker.core.analysis.IDependency;
import org.bundlemaker.core.analysis.IRootArtifact;
import org.bundlemaker.core.selection.Selection;
import org.bundlemaker.core.selection.stage.ArtifactStageChangedEvent;
import org.bundlemaker.core.selection.stage.IArtifactStage;
import org.bundlemaker.core.selection.stage.IArtifactStageChangeListener;
import org.bundlemaker.core.ui.artifact.ArtifactImages;
import org.bundlemaker.core.ui.editor.dependencyviewer.DependencyViewerEditor;
import org.bundlemaker.core.ui.view.ArtifactStageActionHelper;
import org.bundlemaker.core.ui.view.dependencytable.ArtifactPathLabelGenerator;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import com.mxgraph.layout.mxCircleLayout;
import com.mxgraph.layout.mxIGraphLayout;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.swing.mxGraphOutline;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxGraphView;
import com.mxgraph.view.mxStylesheet;
/**
* @author Nils Hartmann (nils@nilshartmann.net)
*
*/
public class DependencyViewerGraph {
private final static String BUNDLEMAKER_VERTEX_STYLE = "BUNDLEMAKER_VERTEX";
private final static String BUNDLEMAKER_EDGE_STYLE = "BUNDLEMAKER_EDGE";
private final static String BUNDLEMAKER_CIRCULAR_EDGE_STYLE = "BUNDLEMAKER_CIRCULAR_EDGE";
private final Map<IBundleMakerArtifact, Object> _vertexCache = new Hashtable<IBundleMakerArtifact, Object>();
private final EdgeCache _edgeCache = new EdgeCache();
private mxGraphComponent _graphComponent;
private mxGraph _graph;
private mxIGraphLayout _graphLayout;
private Display _display;
private UnstageAction _unstageAction;
protected boolean _autoFit = true;
private ArtifactPathLabelGenerator _labelGenerator = new ArtifactPathLabelGenerator();
/**
* Should the graph be re-layouted after artifacts have been added or removed?
*/
private boolean _doLayoutAfterArtifactsChange = true;
public void create(Frame parentFrame, Display display) {
_display = display;
_graph = createGraph();
registerStyles();
// Layout
_graphLayout = new mxCircleLayout(_graph);
_graphComponent = new mxGraphComponent(_graph);
_graphComponent.setConnectable(false);
_graphComponent.setToolTips(true);
_graphComponent.getViewport().setOpaque(true);
_graphComponent.getViewport().setBackground(Color.WHITE);
_graphComponent.setTripleBuffered(true);
_graphComponent.addMouseWheelListener(new ZoomMouseWheelListener());
if (_autoFit) {
_graphComponent.addComponentListener(new InitialComponentResizeListener());
}
// Populate the frame
parentFrame.setLayout(new BorderLayout());
parentFrame.add(createToolBar(), BorderLayout.NORTH);
parentFrame.add(_graphComponent, BorderLayout.CENTER);
}
protected JPanel createToolBar() {
JPanel comboBoxPanel = new JPanel();
// Layout Selector
Vector<GraphLayout> layouts = Layouts.createLayouts(_graphComponent);
final JComboBox comboBox = new JComboBox(layouts);
comboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
GraphLayout newLayout = (GraphLayout) comboBox.getSelectedItem();
_graphLayout = newLayout.getLayout();
DependencyViewerGraph.this.layoutGraph();
}
});
comboBoxPanel.add(comboBox);
// Zoom Action
comboBoxPanel.add(new JButton(new ZoomAction("-", "Zoom out (Ctrl+Mouse Wheel)")));
comboBoxPanel.add(new JButton(new ZoomAction("0", "Reset zoom")));
comboBoxPanel.add(new JButton(new ZoomAction("+", "Zoom in (Ctrl+Mouse Wheel)")));
comboBoxPanel.add(new JButton(new ZoomAction("Fit", "Zoom to fit (horizontal)")));
JCheckBox jCheckBox = new JCheckBox(new AutoFitAction());
jCheckBox.setSelected(_autoFit);
comboBoxPanel.add(jCheckBox);
// UnstageButton
_unstageAction = new UnstageAction();
comboBoxPanel.add(new JButton(_unstageAction));
return comboBoxPanel;
}
public void dispose() {
if (_unstageAction != null) {
_unstageAction.dispose();
}
}
protected mxGraph createGraph() {
mxGraph graph = new mxGraph() {
@Override
public String getToolTipForCell(Object cell) {
Object cellValue = model.getValue(cell);
if (cellValue instanceof IBundleMakerArtifact) {
IBundleMakerArtifact artifact = (IBundleMakerArtifact) cellValue;
return _labelGenerator.getLabel(artifact);
// return ((IBundleMakerArtifact) cellValue).getQualifiedName();
}
if (cellValue instanceof IDependency) {
IDependency dependency = (IDependency) cellValue;
String string = "<html>" + dependency.getFrom().getName() + " -> " + dependency.getTo() + ": "
+ dependency.getWeight();
IDependency dependencyTo = dependency.getTo().getDependencyTo(dependency.getFrom());
if (dependencyTo != null) {
string += "<br/>" + dependencyTo.getFrom().getName() + " -> " + dependencyTo.getTo() + ": "
+ dependencyTo.getWeight();
}
return string + "</html>";
}
return super.getToolTipForCell(cell);
}
@Override
public String convertValueToString(Object cell) {
Object result = model.getValue(cell);
if (result instanceof IBundleMakerArtifact) {
return ((IBundleMakerArtifact) result).getName();
}
return super.convertValueToString(cell);
}
};
// listener for cell selection changes
graph.getSelectionModel().addListener(mxEvent.CHANGE, new mxIEventListener() {
@Override
public void invoke(Object sender, mxEventObject evt) {
cellSelectionChanged();
}
});
// Configure Graph
graph.setCellsDisconnectable(false);
graph.setConnectableEdges(false);
graph.setCellsBendable(false);
graph.setCellsEditable(false);
graph.setCellsResizable(false);
graph.setDropEnabled(false);
return graph;
}
protected void registerStyles() {
// Styles
mxStylesheet stylesheet = _graph.getStylesheet();
// Base style for an Artifact
Hashtable<String, Object> style = new Hashtable<String, Object>();
style.put(mxConstants.STYLE_SHAPE, mxConstants.SHAPE_LABEL);
style.put(mxConstants.STYLE_IMAGE_ALIGN, mxConstants.ALIGN_LEFT);
style.put(mxConstants.STYLE_OPACITY, 50);
style.put(mxConstants.STYLE_FONTCOLOR, "#000000");
style.put(mxConstants.STYLE_FILLCOLOR, "#FFEAB2");
style.put(mxConstants.STYLE_STROKECOLOR, "#C37D64");
style.put(mxConstants.STYLE_STROKEWIDTH, "1");
style.put(mxConstants.STYLE_FONTSIZE, "12");
stylesheet.putCellStyle("BUNDLEMAKER_VERTEX", style);
// base style for a uni-directional dependency
style = new Hashtable<String, Object>();
style.put(mxConstants.STYLE_FONTCOLOR, "#000000");
style.put(mxConstants.STYLE_STROKECOLOR, "#000000"); // #FFEAB2");
style.put(mxConstants.STYLE_STROKEWIDTH, "1");
style.put(mxConstants.STYLE_NOLABEL, "1");
stylesheet.putCellStyle("BUNDLEMAKER_EDGE", style);
// base style for a circular dependency
style = new Hashtable<String, Object>();
style.put(mxConstants.STYLE_FONTCOLOR, "#000000");
style.put(mxConstants.STYLE_STROKECOLOR, "#B85E3D");
style.put(mxConstants.STYLE_STROKEWIDTH, "1");
style.put(mxConstants.STYLE_FILLCOLOR, "#B85E3D");
style.put(mxConstants.STYLE_NOLABEL, "1");
style.put(mxConstants.STYLE_ENDARROW, mxConstants.NONE);
style.put(mxConstants.STYLE_STARTARROW, mxConstants.NONE);
stylesheet.putCellStyle("BUNDLEMAKER_CIRCULAR_EDGE", style);
}
/**
* @param effectiveSelectedArtifacts
*/
public void showArtifacts(List<IBundleMakerArtifact> effectiveSelectedArtifacts) {
Object parent = _graph.getDefaultParent();
mxIGraphModel model = _graph.getModel();
if (effectiveSelectedArtifacts.size() > 0) {
IBundleMakerArtifact anArtifact = effectiveSelectedArtifacts.get(0);
IRootArtifact root = anArtifact.getRoot();
_labelGenerator.setBaseArtifact(root);
}
model.beginUpdate();
try {
Iterator<Entry<IBundleMakerArtifact, Object>> iterator = _vertexCache.entrySet().iterator();
while (iterator.hasNext()) {
Entry<IBundleMakerArtifact, Object> entry = iterator.next();
IBundleMakerArtifact artifact = entry.getKey();
if (!effectiveSelectedArtifacts.contains(artifact)) {
// Artifact is not longer part of selection => remove it...
// ..from model
model.remove(entry.getValue());
// ..from vertex cache
iterator.remove();
// all conntected edges
_edgeCache.removeEdgesConnectedTo(artifact);
}
}
for (IBundleMakerArtifact iBundleMakerArtifact : effectiveSelectedArtifacts) {
if (!_vertexCache.containsKey(iBundleMakerArtifact)) {
String style = BUNDLEMAKER_VERTEX_STYLE;
ArtifactImages image = ArtifactImages.forArtifact(iBundleMakerArtifact);
style += ";image=" + image.getImageUrl();
Rectangle bounds = image.getImage().getBounds();
style += ";imageWidth=" + bounds.width;
style += ";imageWidth=" + bounds.height;
style += ";imageVerticalAlign=center;fontStyle=1;verticalAlign=top;spacingLeft=" + (bounds.width + 15)
+ ";spacingTop=2;imageAlign=left;align=top;spacingRight=5"; // +
Object vertex = _graph.insertVertex(parent, null, iBundleMakerArtifact, 10, 10, 10, 10, style);
_graph.updateCellSize(vertex);
_vertexCache.put(iBundleMakerArtifact, vertex);
}
}
for (IBundleMakerArtifact from : effectiveSelectedArtifacts) {
Collection<IDependency> dependenciesTo = from.getDependenciesTo(effectiveSelectedArtifacts);
Object fromVertex = _vertexCache.get(from);
for (IDependency iDependency : dependenciesTo) {
IBundleMakerArtifact to = iDependency.getTo();
if (from.equals(to)) {
continue;
}
Object edge = _edgeCache.getEdge(from, to);
if (edge == null) {
String style = null;
if (to.getDependencyTo(from) == null) {
style = BUNDLEMAKER_EDGE_STYLE;
} else {
style = BUNDLEMAKER_CIRCULAR_EDGE_STYLE;
}
Object toVertex = _vertexCache.get(to);
edge = _graph.insertEdge(parent, null, iDependency, fromVertex, toVertex, style);
_edgeCache.addEdge(edge);
}
}
}
if (_doLayoutAfterArtifactsChange) {
layoutGraph();
}
} finally {
model.endUpdate();
}
if (_autoFit) {
zoomToFitHorizontal();
}
}
/**
* Handles changes of the cell (edges or vertex) selection
*/
protected void cellSelectionChanged() {
Object[] cells = _graph.getSelectionCells();
final List<IBundleMakerArtifact> selectedArtifacts = new LinkedList<IBundleMakerArtifact>();
final Set<IDependency> selectedDependencies = new LinkedHashSet<IDependency>();
// collect selected artifacts and dependencies
for (Object cell : cells) {
Object value = _graph.getModel().getValue(cell);
if (value instanceof IDependency) {
IDependency dependency = (IDependency) value;
selectedDependencies.add(dependency);
IDependency dependencyTo = dependency.getTo().getDependencyTo(dependency.getFrom());
if (dependencyTo != null) {
selectedDependencies.add(dependencyTo);
}
} else if (value instanceof IBundleMakerArtifact) {
IBundleMakerArtifact bundleMakerArtifact = (IBundleMakerArtifact) value;
selectedArtifacts.add(bundleMakerArtifact);
selectedDependencies.addAll(bundleMakerArtifact.getDependenciesFrom());
selectedDependencies.addAll(bundleMakerArtifact.getDependenciesTo());
}
}
// propagate selected dependencies
runInSwt(new Runnable() {
@Override
public void run() {
Selection
.instance()
.getDependencySelectionService()
.setSelection(Selection.MAIN_DEPENDENCY_SELECTION_ID, DependencyViewerEditor.DEPENDENCY_VIEWER_EDITOR_ID,
selectedDependencies);
}
});
//
_unstageAction.setUnstageCandidates(selectedArtifacts);
}
/**
* (Re-)layouts the whole graph.
*
* <p>
* This methods uses the currently selected layout. It executes the layout regardless of the current
* {@link #_doLayoutAfterArtifactsChange} setting
*/
protected void layoutGraph() {
_graph.getModel().beginUpdate();
try {
_graphLayout.execute(_graph.getDefaultParent());
} finally {
_graph.getModel().endUpdate();
}
}
/**
* @return the {@link IArtifactStage}
*/
protected IArtifactStage getArtifactStage() {
return Selection.instance().getArtifactStage();
}
/**
*
*/
public void zoomToFitHorizontal() {
mxGraphView view = _graph.getView();
int compLen = _graphComponent.getWidth();
int viewLen = (int) view.getGraphBounds().getWidth();
if (compLen == 0 || viewLen == 0) {
return;
}
double scale = (double) compLen / viewLen * view.getScale();
System.out.println("compLen: " + compLen + ", viewLen: " + viewLen + ", scale: " + scale);
if (scale > 1) {
_graphComponent.zoomActual();
} else {
view.setScale(scale);
}
}
/**
* Runs the specified {@link Runnable} on the SWT Thread
*/
protected void runInSwt(final Runnable runnable) {
_display.asyncExec(runnable);
}
class UnstageAction extends AbstractAction implements Runnable, IArtifactStageChangeListener {
private static final long serialVersionUID = 1L;
private List<IBundleMakerArtifact> _unstageCandidates;
public UnstageAction() {
super("Unstage");
// getArtifactStage().addArtifactStageChangeListener(this);
refreshEnablement();
}
/**
* @param selectedArtifacts
*/
public void setUnstageCandidates(List<IBundleMakerArtifact> selectedArtifacts) {
_unstageCandidates = selectedArtifacts;
refreshEnablement();
}
public void dispose() {
// getArtifactStage().removeArtifactStageChangeListener(this);
_unstageCandidates = null;
}
protected void refreshEnablement() {
// if (getArtifactStage().getAddMode().isAutoAddMode()) {
// setEnabled(false);
// return;
// }
setEnabled(_unstageCandidates != null && _unstageCandidates.size() > 0);
}
@Override
public void actionPerformed(ActionEvent arg0) {
// Changes in Stage are processed in SWT, so make sure, we're on the SWT thread
runInSwt(this);
}
@Override
public void run() {
try {
if (!ArtifactStageActionHelper.switchToManualAddModeIfRequired()) {
return;
}
DependencyViewerGraph.this._doLayoutAfterArtifactsChange = false;
getArtifactStage().removeStagedArtifacts(_unstageCandidates);
} finally {
DependencyViewerGraph.this._doLayoutAfterArtifactsChange = true;
}
}
@Override
public void artifactStateChanged(ArtifactStageChangedEvent event) {
refreshEnablement();
}
}
class AutoFitAction extends AbstractAction {
private static final long serialVersionUID = 1L;
public AutoFitAction() {
super("Auto Fit");
putValue(Action.SHORT_DESCRIPTION, "Auto Fit horizontal when content change");
}
/*
* (non-Javadoc)
*
* @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
*/
@Override
public void actionPerformed(ActionEvent e) {
JCheckBox box = (JCheckBox) e.getSource();
_autoFit = box.isSelected();
}
}
class ZoomAction extends AbstractAction {
private static final long serialVersionUID = 1L;
public ZoomAction(String text, String tooltipText) {
super(text);
putValue(Action.SHORT_DESCRIPTION, tooltipText);
}
@Override
public void actionPerformed(ActionEvent arg0) {
String name = (String) getValue(Action.NAME);
if ("+".equals(name)) {
_graphComponent.zoomIn();
} else if ("-".equals(name)) {
_graphComponent.zoomOut();
} else if ("fit".equalsIgnoreCase(name)) {
zoomToFitHorizontal();
} else {
_graphComponent.zoomActual();
}
}
}
/**
* Zoom in/out using the mouse wheel while pressing the ctrl-key
*/
class ZoomMouseWheelListener implements MouseWheelListener {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getSource() instanceof mxGraphOutline || e.isControlDown()) {
if (e.getWheelRotation() < 0) {
_graphComponent.zoomIn();
} else {
_graphComponent.zoomOut();
}
}
}
};
class EdgeCache {
private final List<Object> _edges = new LinkedList<Object>();
public void removeEdgesConnectedTo(IBundleMakerArtifact artifact) {
Iterator<Object> iterator = _edges.iterator();
mxIGraphModel model = _graph.getModel();
while (iterator.hasNext()) {
Object cell = iterator.next();
IDependency dependency = (IDependency) model.getValue(cell);
if (artifact.equals(dependency.getTo()) || artifact.equals(dependency.getFrom())) {
model.remove(cell);
iterator.remove();
}
}
}
/**
* @param edgeFromTo
*/
public void addEdge(Object edgeFromTo) {
_edges.add(edgeFromTo);
}
/**
* @param dependencyOne
* @param dependencyTwo
*/
public Object getEdge(IBundleMakerArtifact dependencyOne, IBundleMakerArtifact dependencyTwo) {
mxIGraphModel model = _graph.getModel();
for (Object edge : _edges) {
IDependency dependency = (IDependency) model.getValue(edge);
if (dependencyOne.equals(dependency.getFrom()) && dependencyTwo.equals(dependency.getTo())) {
return edge;
}
if (dependencyTwo.equals(dependency.getFrom()) && dependencyOne.equals(dependency.getTo())) {
return edge;
}
}
return null;
}
}
class InitialComponentResizeListener implements ComponentListener {
@Override
public void componentHidden(ComponentEvent e) {
}
@Override
public void componentMoved(ComponentEvent e) {
}
@Override
public void componentResized(ComponentEvent e) {
e.getComponent().removeComponentListener(this);
if (_autoFit) {
zoomToFitHorizontal();
}
}
@Override
public void componentShown(ComponentEvent e) {
}
}
}