/*******************************************************************************
* Copyright (c) 2014, 2016 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 (hbz) - initial API & implementation
* Matthias Wienand (itemis AG) - Refactorings and cleanups
* Alexander Nyßen (itemis AG) - Refactorings and cleanups
* Tamas Miklossy (itemis AG) - Refactoring of preferences (bug #446639)
* - Render embedded dot graphs in native mode (bug #493694)
*
*******************************************************************************/
package org.eclipse.gef.dot.internal.ui;
import java.io.File;
import java.net.MalformedURLException;
import java.util.List;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.gef.dot.internal.DotExecutableUtils;
import org.eclipse.gef.dot.internal.DotExtractor;
import org.eclipse.gef.dot.internal.DotFileUtils;
import org.eclipse.gef.dot.internal.DotImport;
import org.eclipse.gef.dot.internal.ui.language.internal.DotActivator;
import org.eclipse.gef.fx.nodes.InfiniteCanvas;
import org.eclipse.gef.graph.Graph;
import org.eclipse.gef.mvc.fx.ui.actions.FitToViewportActionGroup;
import org.eclipse.gef.mvc.fx.ui.actions.ScrollActionGroup;
import org.eclipse.gef.mvc.fx.ui.actions.ZoomActionGroup;
import org.eclipse.gef.mvc.fx.viewer.InfiniteCanvasViewer;
import org.eclipse.gef.zest.fx.ui.ZestFxUiModule;
import org.eclipse.gef.zest.fx.ui.parts.ZestFxUiView;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Link;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.ISelectionService;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.xtext.ui.editor.XtextEditor;
import com.google.inject.Guice;
import com.google.inject.util.Modules;
import javafx.application.Platform;
import javafx.scene.Scene;
/**
* Render DOT content with ZestFx and Graphviz
*
* @author Fabian Steeg (fsteeg)
* @author Alexander Nyßen (anyssen)
*
*/
/* provisional API */@SuppressWarnings("restriction")
public class DotGraphView extends ZestFxUiView {
public static final String STYLES_CSS_FILE = DotGraphView.class
.getResource("styles.css") //$NON-NLS-1$
.toExternalForm();
private static final String EXTENSION = "dot"; //$NON-NLS-1$
private static final String LOAD_DOT_FILE = DotUiMessages.DotGraphView_0;
private static final String SYNC_IMPORT_DOT = DotUiMessages.DotGraphView_1;
private static final String GRAPH_NONE = DotUiMessages.DotGraphView_2;
private static final String GRAPH_RESOURCE = DotUiMessages.DotGraphView_3;
private boolean listenToDotContent = false;
private String currentDot = "digraph{}"; //$NON-NLS-1$
private File currentFile = null;
private Link resourceLabel = null;
private Dot2ZestGraphCopier dot2ZestGraphCopier = new Dot2ZestGraphCopier();
private IPropertyChangeListener preferenceChangeListener = new IPropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) {
if (event.getProperty()
.equals(GraphvizPreferencePage.DOT_PATH_PREF_KEY)) {
// we may enter or leave native mode, so update the graph
updateGraph(currentFile);
}
}
};
private ZoomActionGroup zoomActionGroup;
private FitToViewportActionGroup fitToViewportActionGroup;
private ScrollActionGroup scrollActionGroup;
public DotGraphView() {
super(Guice.createInjector(Modules.override(new DotGraphViewModule())
.with(new ZestFxUiModule())));
}
@Override
protected void activate() {
super.activate();
List<Graph> importDot = new DotImport().importDot(currentDot);
setGraph(importDot.isEmpty() ? null : importDot.get(0));
}
@Override
public void init(IViewSite site) throws PartInitException {
super.init(site);
GraphvizPreferencePage.dotUiPrefStore()
.addPropertyChangeListener(preferenceChangeListener);
}
protected boolean isNativeMode() {
return GraphvizPreferencePage.isGraphvizConfigured();
}
@Override
public void dispose() {
GraphvizPreferencePage.dotUiPrefStore()
.removePropertyChangeListener(preferenceChangeListener);
currentDot = null;
currentFile = null;
if (fitToViewportActionGroup != null) {
fitToViewportActionGroup.dispose();
fitToViewportActionGroup = null;
}
if (zoomActionGroup != null) {
zoomActionGroup.dispose();
zoomActionGroup = null;
}
if (scrollActionGroup != null) {
scrollActionGroup.dispose();
scrollActionGroup = null;
}
getContentViewer().contentsProperty().clear();
super.dispose();
}
@Override
public void createPartControl(final Composite parent) {
super.createPartControl(parent);
// actions
Action updateToggleAction = new UpdateToggle().action(this);
Action loadFileAction = new LoadFile().action(this);
add(updateToggleAction, ISharedImages.IMG_ELCL_SYNCED);
add(loadFileAction, ISharedImages.IMG_OBJ_FILE);
IActionBars actionBars = getViewSite().getActionBars();
IToolBarManager mgr = actionBars.getToolBarManager();
mgr.add(new Separator());
zoomActionGroup = new ZoomActionGroup();
zoomActionGroup.init(getContentViewer());
fitToViewportActionGroup = new FitToViewportActionGroup();
fitToViewportActionGroup.init(getContentViewer());
scrollActionGroup = new ScrollActionGroup();
scrollActionGroup.init(getContentViewer());
zoomActionGroup.fillActionBars(actionBars);
mgr.add(new Separator());
fitToViewportActionGroup.fillActionBars(actionBars);
mgr.add(new Separator());
scrollActionGroup.fillActionBars(actionBars);
// controls
parent.setLayout(new GridLayout(1, true));
initResourceLabel(parent, loadFileAction, updateToggleAction);
getCanvas().setLayoutData(new GridData(GridData.FILL_BOTH));
// scene
Scene scene = getContentViewer().getCanvas().getScene();
scene.getStylesheets().add(STYLES_CSS_FILE);
}
private void initResourceLabel(final Composite parent,
final Action loadAction, final Action toggleAction) {
resourceLabel = new Link(parent, SWT.WRAP);
resourceLabel.setText(GRAPH_NONE);
resourceLabel.addSelectionListener(new SelectionListener() {
@Override
public void widgetSelected(SelectionEvent e) {
processEvent(loadAction, toggleAction, GRAPH_NONE, e);
}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
processEvent(loadAction, toggleAction, GRAPH_NONE, e);
}
private void processEvent(final Action loadFileAction,
final Action toggleAction, final String label,
SelectionEvent e) {
/*
* As we use a single string for the links for localization, we
* don't compare specific strings but say the first link
* triggers the loadAction, else the toggleAction:
*/
if (label.replaceAll("<a>", "").startsWith(e.text)) { //$NON-NLS-1$ //$NON-NLS-2$
loadFileAction.run();
} else {
// toggle as if the button was pressed, then run the action:
toggleAction.setChecked(!toggleAction.isChecked());
toggleAction.run();
}
}
});
resourceLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
}
private void add(Action action, String imageName) {
action.setId(action.getText());
if (imageName != null) {
action.setImageDescriptor(PlatformUI.getWorkbench()
.getSharedImages().getImageDescriptor(imageName));
}
IToolBarManager mgr = getViewSite().getActionBars().getToolBarManager();
mgr.add(action);
}
private void setGraphAsync(final String dot, final File file) {
getViewSite().getShell().getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (!dot.trim().isEmpty()) {
try {
List<Graph> importDot = new DotImport().importDot(dot);
setGraph(importDot.isEmpty() ? null : importDot.get(0));
} catch (Exception e) {
e.printStackTrace();
String message = String.format(
"Could not import DOT: %s, DOT: %s", //$NON-NLS-1$
e.getMessage(), dot);
DotActivator.getInstance().getLog()
.log(new Status(
Status.ERROR, DotActivator.getInstance()
.getBundle().getSymbolicName(),
message));
return;
}
resourceLabel.setText(
String.format(GRAPH_RESOURCE, file.getName())
+ (isNativeMode() ? " [native]" //$NON-NLS-1$
: " [emulated]")); //$NON-NLS-1$
resourceLabel.setToolTipText(file.getAbsolutePath());
}
}
});
}
@Override
public void setGraph(Graph graph) {
// do no convert layout algorithm and rankdir in emulated mode, invert
// y-axis mode (as by default y-axis is interpreted inverse in dot)
boolean isNativeMode = isNativeMode();
dot2ZestGraphCopier.getAttributeCopier()
.options().emulateLayout = !isNativeMode;
dot2ZestGraphCopier.getAttributeCopier().options().invertYAxis = false;
super.setGraph(dot2ZestGraphCopier.copy(graph));
// adjust viewport to scroll to top-left
Platform.runLater(new Runnable() {
@Override
public void run() {
InfiniteCanvas canvas = ((InfiniteCanvasViewer) getContentViewer())
.getCanvas();
canvas.setHorizontalScrollOffset(
canvas.getHorizontalScrollOffset()
- canvas.getContentBounds().getMinX());
canvas.setVerticalScrollOffset(canvas.getVerticalScrollOffset()
- canvas.getContentBounds().getMinY());
}
});
}
private boolean toggle(Action action, boolean input) {
action.setChecked(!action.isChecked());
IToolBarManager mgr = getViewSite().getActionBars().getToolBarManager();
for (IContributionItem item : mgr.getItems()) {
if (item instanceof ActionContributionItem
&& ((ActionContributionItem) item).getAction() == action) {
action.setChecked(!action.isChecked());
return !input;
}
}
return input;
}
private boolean updateGraph(File file) {
if (file == null || !file.exists()) {
return false;
}
currentFile = file;
boolean isEmbeddedDotFile = !currentFile.getName()
.endsWith("." + EXTENSION); //$NON-NLS-1$
DotExtractor dotExtractor = null;
if (isEmbeddedDotFile) {
dotExtractor = new DotExtractor(currentFile);
currentDot = dotExtractor.getDotString();
} else {
currentDot = DotFileUtils.read(currentFile);
}
// if Graphviz 'dot' executable is available, we use it for layout
// (native mode); otherwise we emulate layout with GEF Layout
// algorithms.
if (isNativeMode()) {
// System.out.println("[DOT Input] [" + currentDot + "]");
String[] result;
if (isEmbeddedDotFile) {
File tempDotFile = dotExtractor.getDotTempFile();
if (tempDotFile == null) {
return false;
}
result = DotExecutableUtils.executeDot(
new File(GraphvizPreferencePage.getDotExecutablePath()),
true, tempDotFile, null, null);
tempDotFile.delete();
} else {
result = DotExecutableUtils.executeDot(
new File(GraphvizPreferencePage.getDotExecutablePath()),
true, file, null, null);
}
currentDot = result[0];
// System.out.println("[DOT Output] [" + currentDot + "]");
}
setGraphAsync(currentDot, currentFile);
return true;
}
private IWorkspaceRunnable updateGraphRunnable(final File f) {
if (!listenToDotContent
&& !f.getAbsolutePath().toString().endsWith(EXTENSION)) {
return null;
}
IWorkspaceRunnable workspaceRunnable = new IWorkspaceRunnable() {
@Override
public void run(final IProgressMonitor monitor)
throws CoreException {
if (updateGraph(f)) {
currentFile = f;
}
}
};
return workspaceRunnable;
}
private class LoadFile {
private String lastSelection = null;
Action action(final DotGraphView view) {
return new Action(DotGraphView.LOAD_DOT_FILE) {
@Override
public void run() {
FileDialog dialog = new FileDialog(
view.getViewSite().getShell(), SWT.OPEN);
dialog.setFileName(lastSelection);
String dotFileNamePattern = "*." + EXTENSION; //$NON-NLS-1$
String embeddedDotFileNamePattern = "*.*"; //$NON-NLS-1$
dialog.setFilterExtensions(new String[] {
dotFileNamePattern, embeddedDotFileNamePattern });
dialog.setFilterNames(new String[] {
String.format("DOT file (%s)", dotFileNamePattern), //$NON-NLS-1$
String.format("Embedded DOT Graph (%s)", //$NON-NLS-1$
embeddedDotFileNamePattern) });
String selection = dialog.open();
if (selection != null) {
lastSelection = selection;
view.updateGraph(new File(selection));
}
}
};
}
}
private class UpdateToggle {
/** Listener that passes a visitor if a resource is changed. */
private IResourceChangeListener resourceChangeListener;
/** If a *.dot file is visited, update the graph. */
private IResourceDeltaVisitor resourceVisitor;
/** Listen to selection changes and update graph in view. */
protected ISelectionListener selectionChangeListener = null;
Action action(final DotGraphView view) {
Action toggleUpdateModeAction = new Action(
DotGraphView.SYNC_IMPORT_DOT, SWT.TOGGLE) {
@Override
public void run() {
listenToDotContent = toggle(this, listenToDotContent);
toggleResourceListener();
}
private void toggleResourceListener() {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
ISelectionService service = getSite().getWorkbenchWindow()
.getSelectionService();
if (view.listenToDotContent) {
IWorkbenchPart activeEditor = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow().getActivePage()
.getActiveEditor();
checkActiveEditorAndUpdateGraph(activeEditor);
workspace.addResourceChangeListener(
resourceChangeListener,
IResourceChangeEvent.POST_BUILD
| IResourceChangeEvent.POST_CHANGE);
service.addSelectionListener(selectionChangeListener);
} else {
workspace.removeResourceChangeListener(
resourceChangeListener);
service.removeSelectionListener(
selectionChangeListener);
}
}
};
selectionChangeListener = new ISelectionListener() {
@Override
public void selectionChanged(IWorkbenchPart part,
ISelection selection) {
checkActiveEditorAndUpdateGraph(part);
}
};
resourceChangeListener = new IResourceChangeListener() {
@Override
public void resourceChanged(final IResourceChangeEvent event) {
if (event.getType() != IResourceChangeEvent.POST_BUILD
&& event.getType() != IResourceChangeEvent.POST_CHANGE) {
return;
}
IResourceDelta rootDelta = event.getDelta();
try {
rootDelta.accept(resourceVisitor);
} catch (CoreException e) {
e.printStackTrace();
}
}
};
resourceVisitor = new IResourceDeltaVisitor() {
@Override
public boolean visit(final IResourceDelta delta) {
IResource resource = delta.getResource();
if (resource.getType() == IResource.FILE
&& ((IFile) resource).getName()
.endsWith(EXTENSION)) {
try {
final IFile f = (IFile) resource;
IWorkspaceRunnable workspaceRunnable = view
.updateGraphRunnable(DotFileUtils.resolve(
f.getLocationURI().toURL()));
IWorkspace workspace = ResourcesPlugin
.getWorkspace();
if (!workspace.isTreeLocked()) {
workspace.run(workspaceRunnable, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
}
};
return toggleUpdateModeAction;
}
/**
* if the active editor is the DOT Editor, update the graph, otherwise
* do nothing
*/
private void checkActiveEditorAndUpdateGraph(IWorkbenchPart part) {
if (part instanceof XtextEditor) {
XtextEditor editor = (XtextEditor) part;
if (DotActivator.ORG_ECLIPSE_GEF_DOT_INTERNAL_LANGUAGE_DOT
.equals(editor.getLanguageName())
&& editor.getEditorInput() instanceof FileEditorInput) {
try {
File resolvedFile = DotFileUtils.resolve(
((FileEditorInput) editor.getEditorInput())
.getFile().getLocationURI().toURL());
if (!resolvedFile.equals(currentFile)) {
updateGraph(resolvedFile);
}
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
}
}
}