Package com.google.devtools.depan.eclipse.editors

Source Code of com.google.devtools.depan.eclipse.editors.ViewEditor

/*
* Copyright 2007 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.devtools.depan.eclipse.editors;

import com.google.devtools.depan.eclipse.persist.ObjectXmlPersist;
import com.google.devtools.depan.eclipse.persist.XStreamFactory;
import com.google.devtools.depan.eclipse.plugins.SourcePlugin;
import com.google.devtools.depan.eclipse.stats.ElementKindStats;
import com.google.devtools.depan.eclipse.trees.GraphData;
import com.google.devtools.depan.eclipse.utils.ListenerManager;
import com.google.devtools.depan.eclipse.utils.elementkinds.ElementKindDescriptor;
import com.google.devtools.depan.eclipse.utils.elementkinds.ElementKindDescriptors;
import com.google.devtools.depan.eclipse.utils.relsets.RelSetDescriptor;
import com.google.devtools.depan.eclipse.utils.relsets.RelSetDescriptors;
import com.google.devtools.depan.eclipse.views.tools.RelationCount;
import com.google.devtools.depan.eclipse.visualization.View;
import com.google.devtools.depan.eclipse.visualization.layout.LayoutContext;
import com.google.devtools.depan.eclipse.visualization.layout.LayoutGenerator;
import com.google.devtools.depan.eclipse.visualization.layout.LayoutGenerators;
import com.google.devtools.depan.eclipse.visualization.layout.LayoutScaler;
import com.google.devtools.depan.eclipse.visualization.layout.LayoutUtil;
import com.google.devtools.depan.graph.api.DirectedRelationFinder;
import com.google.devtools.depan.graph.api.Relation;
import com.google.devtools.depan.graph.basic.ForwardIdentityRelationFinder;
import com.google.devtools.depan.model.GraphEdge;
import com.google.devtools.depan.model.GraphModel;
import com.google.devtools.depan.model.GraphNode;
import com.google.devtools.depan.model.RelationshipSet;
import com.google.devtools.depan.view.CollapseData;
import com.google.devtools.depan.view.EdgeDisplayProperty;
import com.google.devtools.depan.view.NodeDisplayProperty;
import com.google.devtools.depan.view.TreeModel;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.thoughtworks.xstream.XStream;

import edu.uci.ics.jung.algorithms.importance.KStepMarkov;
import edu.uci.ics.jung.graph.DirectedGraph;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.SaveAsDialog;
import org.eclipse.ui.part.MultiPageEditorPart;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;

/**
* An Editor for a DepAn ViewDocument.
*
* @author ycoppel@google.com (Yohann Coppel)
*/
public class ViewEditor extends MultiPageEditorPart {

  public static final String ID =
      "com.google.devtools.depan.eclipse.editors.ViewEditor";

  private static final Logger logger =
      Logger.getLogger(ViewEditor.class.getName());

  /** How much room to consume for full viewport layout scaling. */
  public static final double FULLSCALE_MARGIN = 0.9;

  /**
   * Defines the OpenGL distance between two points that should be considered
   * equivalent to zero.
   *
   * <p>Ideally, this should be obtained from the GLPanel/View, perhaps as the
   * half the OpenGL distance between two pixels.  But that will have to wait.
   */
  private static final double ZERO_THRESHOLD = 0.1;

  /////////////////////////////////////
  // Editor state for persistence

  /** State of the view.  Only this data is saved. */
  private ViewDocument viewInfo;

  /** Persistent location for the viewInfo. */
  private IFile viewFile;

  /**
   * Base name to use for created files. Typically set through the
   * ViewEditorInput supplied at editor startup.
   */
  private String baseName;

  /** Dirty state. */
  private boolean isDirty = true;

  /////////////////////////////////////
  // Resources to release in the dispose() method

  /** Handle changes to the user preferences, such a node locations. */
  private ViewPrefsListener viewPrefsListener;

  /** Results from defining hierarchies over the nodes. */
  private HierarchyCache<NodeDisplayProperty> hierarchies;

  /** The visualization View that handles rendering. */
  private View renderer;

  /**
   * Forward only selection change events to interested parties.
   */
  private ListenerManager<SelectionChangeListener> selectionListeners =
      new ListenerManager<SelectionChangeListener>();

  private ListenerManager<DrawingListener> drawingListeners =
      new ListenerManager<DrawingListener>();

  /////////////////////////////////////
  // Alternate graph perspectives and derived data
  // used in various tools and viewers

  private GraphModel viewGraph;

  private GraphModel exposedGraph;

  /** Used for rendering and ranking in the rending pipe. */
  private DirectedGraph<GraphNode, GraphEdge> jungGraph;

  private Map<GraphNode, Double> ranking;

  private RelationCount.Settings relationCountData =
    new RelationCount.Settings();

  private List<RelSetDescriptor> relSetChoices;

  private Collection<ElementKindDescriptor> elementKindChoices;

  private Collection<ElementKindStats.Info> elementKindStats;

  /////////////////////////////////////
  // Basic Getters and Setters

  public GraphModel getViewGraph() {
    return viewGraph;
  }

  public GraphModel getParentGraph() {
    return viewInfo.getParentGraph();
  }

  public List<SourcePlugin> getBuiltinAnalysisPlugins() {
    return viewInfo.getBuiltinAnalysisPlugins();
  }

  public Collection<RelationshipSet> getBuiltinAnalysisRelSets() {
    return viewInfo.getBuiltinAnalysisRelSets();
  }

  public List<RelSetDescriptor> getRelSetChoices() {
    return relSetChoices;
  }

  public RelationshipSet getContainerRelSet() {
    return viewInfo.getDefaultContainerRelSet();
  }

  public RelationshipSet getDisplayRelationSet() {
    return viewInfo.getDisplayRelationSet();
  }

  public void setDisplayRelationSet(RelationshipSet newDisplay) {
    viewInfo.setDisplayRelationSet(newDisplay);
  }

  /**
   * @return
   */
  public Collection<ElementKindDescriptor> getElementKinds() {
    return elementKindChoices;
  }

  public Collection<ElementKindStats.Info> getElementKindStats() {
    return elementKindStats;
  }

  public DirectedGraph<GraphNode, GraphEdge> getJungGraph() {
    return jungGraph;
  }

  public Map<GraphNode, Double> getNodeRanking() {
    return ranking;
  }

  /////////////////////////////////////
  // Manage dirty state for editor

  private void setDirtyState(boolean dirty) {
    this.isDirty = dirty;
    firePropertyChange(IEditorPart.PROP_DIRTY);
  }

  @Override
  public boolean isDirty() {
    return isDirty;
  }

  private void markDirty() {
    setDirtyState(true);
  }

  /////////////////////////////////////
  // Eclipse rendering integration

  @Override
  public void setInitializationData(
      IConfigurationElement cfig,
      String propertyName, Object data) {
    super.setInitializationData(cfig, propertyName, data);
  }

  @Override
  protected void createPages() {
    createDiagramPage();
    createDetailsPage();
  }

  private void createDiagramPage() {
    Composite parent = new Composite(getContainer(), SWT.H_SCROLL | SWT.V_SCROLL);

    GridLayout pageLayout = new GridLayout();
    pageLayout.numColumns = 1;
    parent.setLayout(pageLayout);

    // bottom composite containing main diagram
    renderer = new View(parent, SWT.NONE, this);
    renderer.setLayoutData(
        new GridData(SWT.FILL, SWT.FILL, true, true));

    // Configure the rendering pipe before listening for changes.
    renderer.setGraphModel(getViewGraph(), getJungGraph(), getNodeRanking());
    renderer.initCameraPosition(viewInfo.getScenePrefs());
    renderer.initializeNodeLocations(viewInfo.getNodeLocations());
    initSelectedNodes(getSelectedNodes());
    initEdgeRendering();
    renderer.start();

    // Force a layout if there are no locations.
    if (viewInfo.getNodeLocations().isEmpty()) {
      addDrawingListener(new DrawingListener() {

        @Override
        public void updateDrawingBounds(
            Rectangle2D drawing, Rectangle2D viewport) {
          // Don't layout nodes if no layout is defined
          LayoutGenerator selectedLayout = getSelectedLayout();
          if (null == selectedLayout ) {
            return;
          }

          // Run the layout process on all nodes in the view.
          applyLayout(selectedLayout,
              viewInfo.getLayoutFinder(), viewInfo.getViewNodes());

          // Only need to do this once on startup
          removeDrawingListener(this);
        }});
    }

    int index = addPage(parent);
    setPageText(index, "Graph View");
  }

  protected String edgeToolTip(GraphEdge edge) {
    final String str = edge.toString();
    updateStatusLine(str);
    return str;
  }

  protected String vertexToolTip(GraphNode node) {
    final String str = node.toString();
    updateStatusLine(str);
    return str;
  }

  private void createDetailsPage() {
    Composite parent = new Composite(getContainer(), SWT.NONE);
    GridLayout layout = new GridLayout();
    layout.numColumns = 3;
    layout.marginTop = 9;
    parent.setLayout(layout);

    GridData fillGrid = new GridData(SWT.FILL, SWT.FILL, true, false);

    Label nameLabel = new Label(parent, SWT.NONE);
    final Text name = new Text(parent, SWT.BORDER | SWT.SINGLE);

    nameLabel.setText("Description");
    name.setText(viewInfo.getDescription());

    name.setLayoutData(fillGrid);

    name.addModifyListener(new ModifyListener() {

      @Override
      public void modifyText(ModifyEvent e) {
        if (viewInfo != null) {
          String newDescription = name.getText();
          viewInfo.setDescription(newDescription);
          markDirty();
        }
      }
    });
    int index = addPage(parent);
    setPageText(index, "Properties");
  }

  @Override
  public void init(IEditorSite site, IEditorInput input)
      throws PartInitException  {
    super.init(site, input);

    initFromInput(input);

    // Synthesize derived graph perspectives
    deriveDetails();

    // Listen to changes in the underlying ViewModel
    viewPrefsListener = new Listener();
    viewInfo.addPrefsListener(viewPrefsListener);
  }

  private void initFromInput(IEditorInput input) throws PartInitException {
    if (input instanceof ViewEditorInput) {
      ViewEditorInput editorInput = (ViewEditorInput) input;
      viewFile = null; // not yet saved
      baseName = editorInput.getBaseName();
      viewInfo = editorInput.getViewDocument();
      setPartName(calcPartName());
      markDirty();
    } else if (input instanceof IFileEditorInput) {
      try {
        viewFile = ((IFileEditorInput) input).getFile();
        baseName = viewFile.getName();
        viewInfo = loadViewDocument(viewFile);
        setPartName(calcPartName());
        setDirtyState(false);
      } catch (IOException e) {
        viewFile = null;
        viewInfo = null;
        throw new PartInitException(
            "Unable to load view from " + viewFile.getFullPath().toString());
      }
    } else {
      throw new PartInitException(
          "Input for editor is not suitable for the ViewEditor");
    }
  }

  /**
   * Derive a number of alternative presentations and details from the
   * newly open graph view.
   */
  private void deriveDetails() {
    // Synthesize derived graph perspectives
    viewGraph = viewInfo.buildGraphView();
    updateExposedGraph();

    hierarchies = new HierarchyCache<NodeDisplayProperty>(
        viewInfo.getNodeDisplayPropertyProvider(),
        getViewGraph());

    relSetChoices = RelSetDescriptors.buildViewChoices(viewInfo);
    elementKindChoices = ElementKindDescriptors.buildViewChoices(viewInfo);

    ElementKindStats stats = new ElementKindStats(elementKindChoices);
    stats.incrStats(viewInfo.getViewNodes());
    elementKindStats = stats.createStats();

    jungGraph = buildJungGraph();
    ranking = rankGraph(jungGraph);
  }

  /**
   * Associate to each node a value based on it's "importance" in the graph.
   * The selected algorithm is a Page Rank algorithm.
   *
   * The result is stored in the map {@link #ranking}.
   */
  @SuppressWarnings("unused") // Retained legacy code
  private Map<GraphNode, Double> rankGraphX(
          DirectedGraph<GraphNode, GraphEdge> graph) {

    KStepMarkov<GraphNode, GraphEdge> ranker =
        new KStepMarkov<GraphNode, GraphEdge>(graph, null, 6, null);
    ranker.setRemoveRankScoresOnFinalize(false);
    ranker.evaluate();

    Map<GraphNode, Double> result = Maps.newHashMap();
    for (GraphNode node : exposedGraph.getNodes()) {
      result.put(node, ranker.getVertexRankScore(node));
    }

    return result;
  }

  private Map<GraphNode, Double> rankGraph(
      DirectedGraph<GraphNode, GraphEdge> graph) {

    Double unit = 1.0;
    Map<GraphNode, Double> result = Maps.newHashMap();
    for (GraphNode node : exposedGraph.getNodes()) {
      result.put(node, unit);
    }

    return result;
  }

  private DirectedGraph<GraphNode, GraphEdge> buildJungGraph( ) {
    LayoutContext context = new LayoutContext();
    context.setGraphModel(getExposedGraph());
    context.setMovableNodes(viewGraph.getNodes());
    // TODO: Compute ranking based on selected relations
    context.setRelations(ForwardIdentityRelationFinder.FINDER);

    return LayoutUtil.buildJungGraph(context);
  }

  /**
   * Release the resource held by the ViewEditor:
   * - ViewModelListener
   * - Underlying view.
   */
  @Override
  public void dispose() {
    if (null != viewPrefsListener) {
      viewInfo.removePrefsListener(viewPrefsListener);
      viewPrefsListener = new Listener();
    }

    if (null != hierarchies) {
      hierarchies = null;
    }

    if (null != renderer) {
      renderer.dispose();
      renderer = null;
    }

    super.dispose();
  }

  @Override
  public void setFocus() {
  }

  /////////////////////////////////////
  // Actions on the rendering system

  /**
   * Take a screenshot of the given view. Ask the user a filename, and use
   * this filename to determine which type of file format has to be used.
   * PNG format is used as default.
   *
   * @param view the view to capture.
   */
  public void takeScreenshot() {
    // make the screenshot first, so that the overlapping file selection window
    // does not Interfere with the process of taking the screenshot
    // (apparently, otherwise, it does)
    BufferedImage screenshot = renderer.takeScreenshot();

    // ask the user a filename where to save the screenshot
    FileDialog fd = new FileDialog( getSite().getShell(), SWT.SAVE);
    fd.setText("Save Screenshot:");
    String[] filterExt = { "*.png", "*.jpg", "*.gif", "*.bmp", "*.*" };
    fd.setFilterExtensions(filterExt);
    String selected = fd.open();

    IActionBars bars = getEditorSite().getActionBars();

    // user canceled operation. Print a message in the status line, and return.
    if (null == selected) {
      bars.getStatusLineManager().setErrorMessage(
          "To take a screenshot, you must specify a filename.");
      return;
    }

    // check if the file has an extension. otherwise, use .png as default
    // extension.
    if (selected.lastIndexOf('.') == -1) {
      selected = selected+".png";
    }

    try {
      // finally, write the image on a file.
      ImageIO.write(screenshot, selected.substring(
          selected.lastIndexOf('.')+1), new File(selected));
      bars.getStatusLineManager().setMessage("Image saved to "+selected);
    } catch (IOException e) {
      e.printStackTrace();
      bars.getStatusLineManager().setErrorMessage(
          "Error while saving screenshot");
    }
  }

  /////////////////////////////////////
  // Persistence Support

  @Override
  public boolean isSaveAsAllowed() {
    return true;
  }

  /**
   * Load a view document from a file.
   */
  private static ViewDocument loadViewDocument(IFile viewFile)
      throws IOException {
    ObjectXmlPersist persist =
        new ObjectXmlPersist(XStreamFactory.getSharedRefXStream());
    return (ViewDocument) persist.load(viewFile.getRawLocationURI());
  }

  /**
   * Save a view document to a file.
   */
  private static void saveViewDocument(IFile viewFile, ViewDocument viewInfo)
      throws IOException {
    XStream xstream = XStreamFactory.newStaxXStream();
    XStreamFactory.configureRefXStream(xstream);
    ObjectXmlPersist persist = new ObjectXmlPersist(xstream);
    persist.save(viewFile.getRawLocationURI(), viewInfo);
  }

  @Override
  public void doSave(IProgressMonitor monitor) {
    // If there are any file problems, do this as a Save As ..
    if (null == viewFile) {
      doSaveAs();
      return;
    }

    saveFile(viewFile, monitor, "save");
    if (null != monitor) {
      monitor.done();
    }
  }

  @Override
  public void doSaveAs() {
    SaveAsDialog saveas = new SaveAsDialog(getSite().getShell());
    saveas.setOriginalFile(getSaveAsFile());
    saveas.setOriginalName(calcSaveAsName());
    if (saveas.open() != SaveAsDialog.OK) {
      return;
    }

    // get the file relatively to the workspace.
    IFile saveFile = calcViewFile(saveas.getResult());
    // TODO: set up a progress monitor
    saveFile(saveFile, null, "saveAs");

    viewFile = saveFile;
    setPartName(viewFile.getName());
    }

  private IFile getSaveAsFile() {
    if (null != viewFile) {
      return viewFile;
    }

    IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
    IProject[] projs = root.getProjects();

    // Propose a name only if there is exactly one project.
    if (projs.length != 1) {
      return null;
    }

    IProject proj = projs[0];
    String name = calcSaveAsName();
    return proj.getFile(name);
  }

  public String getBaseName() {
    return baseName;
  }

  /**
   * Provide a likely file name for this view editor's contents.
   */
  private String calcSaveAsName() {
    // Basename can be null (on initialization, etc.).
    // Rare, but cope with it.
    if (null != baseName && !baseName.isEmpty()) {
      return baseName;
    }

    String graphName = viewInfo.getGraphModelLocation().getName();
    String label = NewEditorHelper.newEditorLabel(graphName);
    baseName = label;
    return label;
  }

  /**
   * Provide a tab name for this editor./
   */
  private String calcPartName() {
    if (null != baseName && !baseName.isEmpty()) {
      return baseName;
    }

    // TODO: Also set baseName .. see getSaveAsName()
    String graphName = viewInfo.getGraphModelLocation().getName();
    return NewEditorHelper.newEditorLabel(
        graphName + " - New View");
  }

  /**
   * Save the view document, managing the progress monitor too.
   * Trigger a resource refresh for the supplied file if the save is successful.
   *
   * @param file where to save the view document
   * @param monitor progress indicator to update
   * @param opLabel save operation being performed
   * @throws IOException if the save is unsuccessful
   */
  private void saveFile(IFile file, IProgressMonitor monitor, String opLabel) {
    if (null != monitor)
      monitor.setTaskName("Writing file " + file.getName());

    try {
      saveViewDocument(file, viewInfo);
    } catch (IOException err) {
      logger.log(Level.SEVERE,
          "Unable to " + opLabel + " " + file.getName(), err);
      if (null != monitor)
        monitor.setCanceled(true);
    }

    try {
      // WEIRD:  refreshLocal() directly on the resource often works,
      // but here we sometimes get a conflict.
      file.refreshLocal(1, monitor);
    } catch (CoreException errCore) {
      logger.log(Level.WARNING,
          "Failed resource refresh after " + opLabel + " to "
              + file.getFullPath().toString(),
          errCore);
    }

    setDirtyState(false);
  }

  /**
   * Ensure that we have a file extension on the file name.
   *
   * @param savePath Initial save path from user
   * @return valid IFile with an extension.
   */
  private IFile calcViewFile(IPath savePath) {
    if (null == savePath.getFileExtension()) {
      savePath = savePath.addFileExtension(ViewDocument.EXTENSION);
    }
    return ResourcesPlugin.getWorkspace().getRoot().getFile(savePath);
  }

  /////////////////////////////////////
  // Graph elements

  /**
   * Provide a {@code NodeDisplayProperty} for any {@code GraphNode}.
   * If there is no persistent value, synthesize one, but it won't be saved
   * until some process (e.g. NodeEditor) modifies it's properties.
   */
  public NodeDisplayProperty getNodeProperty(GraphNode node) {
    NodeDisplayProperty result = viewInfo.getNodeProperty(node);
    if (null != result) {
      return result;
    }
    return new NodeDisplayProperty();
  }

  public void setNodeProperty(
      GraphNode node, NodeDisplayProperty newProperty) {
    viewInfo.setNodeProperty(node, newProperty);
  }

  /**
   * Provide an {@code EdgeDisplayProperty} for any {@code GraphEdge}.
   * If there is no persistent value, synthesize one, but it won't be save
   * until some process (e.g. EdgeEditor) modifies it's properties.
   */
  public EdgeDisplayProperty getEdgeProperty(GraphEdge edge) {
    EdgeDisplayProperty result = viewInfo.getEdgeProperty(edge);
    if (null != result) {
      return result;
    }
    return new EdgeDisplayProperty();
  }

  public void setEdgeProperty(
      GraphEdge edge, EdgeDisplayProperty newProperty) {
    viewInfo.setEdgeProperty(edge, newProperty);
  }

  public void setRelationVisible(Relation relation, boolean isVisible) {
    EdgeDisplayProperty edgeProp = getRelationProperty(relation);
    if (null == edgeProp) {
      edgeProp = new EdgeDisplayProperty();
      edgeProp.setVisible(isVisible);
      viewInfo.setRelationProperty(relation, edgeProp);
      return;
    }

    // Don't change a relation that is already visible
    if (edgeProp.isVisible() == isVisible) {
      return;
    }
    edgeProp.setVisible(isVisible);
    viewInfo.setRelationProperty(relation, edgeProp);
  }

  public Collection<Relation> getDisplayRelations() {
    return viewInfo.getDisplayRelations();
  }

  public EdgeDisplayProperty getRelationProperty(Relation relation) {
    return viewInfo.getRelationProperty(relation);
  }

  private void initEdgeRendering() {
    for (GraphEdge edge : viewGraph.getEdges()) {

      // If the edge has explicit display properties, use those.
      EdgeDisplayProperty edgeProp = viewInfo.getEdgeProperty(edge);
      if (null != edgeProp) {
        renderer.updateEdgeProperty(edge, edgeProp);
        continue;
      }

      EdgeDisplayProperty relationProp =
          getRelationProperty(edge.getRelation());
      if (null == relationProp) {
        renderer.setEdgeVisible(edge, false);
        continue;
      }

      renderer.updateEdgeProperty(edge, relationProp);
    }
  }

  private void updateEdgesToRelations() {
    for (GraphEdge edge : viewGraph.getEdges()) {
      // If the edge has explicit display properties, leave those.
      EdgeDisplayProperty edgeProp = viewInfo.getEdgeProperty(edge);
      if (null != edgeProp) {
        continue;
      }

      EdgeDisplayProperty relationProp =
          getRelationProperty(edge.getRelation());
      if (null == relationProp) {
        renderer.setEdgeVisible(edge, false);
        continue;
      }

      renderer.updateEdgeProperty(edge, relationProp);
    }
  }

  /////////////////////////////////////
  // Collapsed presentations

  public GraphModel getExposedGraph() {
    return exposedGraph;
  }

  public void autoCollapse(DirectedRelationFinder finder, Object author) {
    GraphData<NodeDisplayProperty> tree = hierarchies.getHierarchy(finder);
    TreeModel treeData = tree.getTreeModel();
    collapseTree(treeData, author);
  }

  public void collapseTree(TreeModel treeData, Object author) {
    viewInfo.collapseTree(getViewGraph(), treeData, author);
  }

  public void collapse(
      GraphNode master, Collection<GraphNode> picked,
      boolean erase, Object author) {
    viewInfo.collapse(master, picked, erase, author);
  }

  public void uncollapse(
      GraphNode master, boolean deleteGroup, Object author) {
    viewInfo.uncollapse(master, deleteGroup, author);
  }

  private void updateExposedGraph() {
    exposedGraph = viewInfo.buildExposedGraph(viewGraph);
  }

  /////////////////////////////////////
  // Update Graph Layouts

  public String getLayoutName() {
    return viewInfo.getSelectedLayout();
  }

  public LayoutGenerator getSelectedLayout() {
    return LayoutGenerators.getByName(getLayoutName());
  }

  public Map<GraphNode, Point2D> getNodeLocations() {
    return viewInfo.getNodeLocations();
  }
  /////////////////////////////////////
  // Update node positions in the View Document

  /**
   * Zoom to supplied scale.
   * A value of 1.0 places the camera at the default location.
   */
  public void setZoom(float scale) {
    renderer.setZoom(scale);
  }

  public void editNodeLocations(
      Map<GraphNode, Point2D> nodeLocations, Object author) {
    viewInfo.editNodeLocations(nodeLocations, author);
  }

  /**
   * For internal use to avoid the null author.
   */
  private void editNodeLocations(Map<GraphNode, Point2D> nodeLocations) {
    editNodeLocations(nodeLocations, null);
  }

  /**
   * Scale the exposed nodes so they would fit in the viewport, if the viewport
   * was centered over the nodes.  This has been the historical behavior of the
   * {@code FactorPlugin}.
   */
  public void scaleToViewport() {
    scaleToViewport(getExposedGraph().getNodes(), getNodeLocations());
  }

  /**
   * Scale the coordinates for all exposed nodes as indicated by the parameters.
   *
   * @param scaleX scale factor for X coordinates
   * @param scaleY scale factor for Y coordinates
   */
  public void scaleLayout(double scaleX, double scaleY) {
    Point2dUtils.Translater translater =
        Point2dUtils.newScaleTranslater(scaleX, scaleY);
    Map<GraphNode, Point2D> changes = Point2dUtils.translateNodes(
        getExposedGraph().getNodes(), getNodeLocations(),
        translater);

    editNodeLocations(changes);
  }

  private void scaleToViewport(
      Collection<GraphNode> layoutNodes, Map<GraphNode, Point2D> locations) {
    Rectangle2D viewport = renderer.getOGLViewport();
    Map<GraphNode, Point2D> changes =
            computeFullViewScale(layoutNodes, locations, viewport);
    editNodeLocations(changes);
  }

  /**
   * Scale the nodes so that they would fit in the viewport, but don't force
   * them into the viewport.
   *
   * @param layoutNodes
   * @param locations
   * @param viewport
   * @return updated node locations that are the same size as the viewport
   */
  private Map<GraphNode, Point2D> computeFullViewScale(
          Collection<GraphNode> layoutNodes,
          Map<GraphNode, Point2D> locations,
          Rectangle2D viewport) {

    if (layoutNodes.size() <= 0) {
      return Collections.emptyMap();
    }

    // If there is only one node, don't change its location
    Map<GraphNode, Point2D> result = Maps.newHashMap();
    if (layoutNodes.size() == 1) {
      GraphNode singletonNode = layoutNodes.iterator().next();
      Point2D singletonLocation = locations.get(singletonNode);
      if (null != singletonLocation) {
        result.put(singletonNode, singletonLocation);
      }
      return result;
    }

    // Scale all the nodes to fit within the indicated region
    LayoutScaler scaler = new LayoutScaler(layoutNodes, locations);
    double scaleView = scaleWithMargin(scaler, viewport);
    Point2dUtils.Translater translater =
            Point2dUtils.newScaleTranslater(scaleView, scaleView);
    return Point2dUtils.translateNodes(layoutNodes, locations, translater);
  }

  private double scaleWithMargin(
          LayoutScaler scaler, Rectangle2D viewport) {
    return FULLSCALE_MARGIN
            * scaler.getFullViewScale(viewport, ZERO_THRESHOLD);
  }


  /////////////////////////////////////
  // Compute new positions based on a LayoutGenerator

  public Point2D getPosition(GraphNode node) {
    return viewInfo.getNodeLocations().get(node);
  }

  public double getXPos(GraphNode node) {
    Point2D position = getPosition(node);
    return position.getX();
  }

  public double getYPos(GraphNode node) {
    Point2D position = getPosition(node);
    return position.getY();
  }

  public void applyLayout(LayoutGenerator layout) {
    applyLayout(layout, viewInfo.getLayoutFinder());
  }

  public void applyLayout(
          LayoutGenerator layout, DirectedRelationFinder relationFinder) {

    Collection<GraphNode> layoutNodes = getLayoutNodes();
    if (layoutNodes.size() < 2) {
      // TODO: Notify user that a single node cannot be positioned.
      return;
    }
    applyLayout(layout, relationFinder, layoutNodes);
  }

  private Collection<GraphNode> getLayoutNodes() {
    Collection<GraphNode> picked = getSelectedNodes();
    if (0 == picked.size()) {
      return getExposedGraph().getNodes();
    }

    return picked;
  }

  /**
   * Apply the given layout with the given {@link DirectedRelationFinder} to
   * build a tree if the layout need one, to the graph.
   *
   * @param layout the new Layout to apply
   * @param relationFinder {@link DirectedRelationFinder} to restrict
   *        relations for layout.
   * @param layoutNodes nodes that participate in the layout
   */
  private void applyLayout(
      LayoutGenerator layout, DirectedRelationFinder relationFinder,
      Collection<GraphNode> layoutNodes) {

    LayoutContext context = new LayoutContext();
    context.setGraphModel(getExposedGraph());
    context.setMovableNodes(layoutNodes);
    context.setRelations(relationFinder);
    context.setNodeLocations(getNodeLocations());

    Rectangle2D viewport = renderer.getOGLViewport();
    Rectangle2D layoutViewport = Point2dUtils.scaleRectangle(viewport, 0.7);
    context.setViewport(layoutViewport);

    Map<GraphNode, Point2D> changes = LayoutUtil.calcPositions(
            layout, context, layoutNodes);

    // Change the node locations.
    editNodeLocations(changes);
  }

  /////////////////////////////////////
  // Node selection and event handling
  // TODO(leeca): Consolidate all Selection change notifications into a
  // uniform data structure (e.g. always Collection<GraphNode> or GraphNode[]).

  public Collection<GraphNode> getSelectedNodes() {
    return viewInfo.getSelectedNodes();
  }

  public boolean isSelected(GraphNode test) {
    return getSelectedNodes().contains(test);
  }

  public void selectAllNodes() {
    selectNodes(viewInfo.getViewNodes(), this);
  }

  public void selectNodes(Collection<GraphNode> nodes) {
    viewInfo.setSelectedNodes(nodes, null);
  }

  public void selectNodes(Collection<GraphNode> nodes, Object author) {
    viewInfo.setSelectedNodes(nodes, author);
  }

  public void extendSelection(
      Collection<GraphNode> extendNodes, Object author) {
    viewInfo.editSelectedNodes(GraphNode.EMPTY_NODE_LIST, extendNodes, author);
  }

  public void reduceSelection(
      Collection<GraphNode> reduceNodes, Object author) {
    viewInfo.editSelectedNodes(reduceNodes, GraphNode.EMPTY_NODE_LIST, author);
  }

  public void moveSelectionDelta(
      double deltaX, double deltaY, Object author) {
    Point2dUtils.Translater translater =
        Point2dUtils.newDeltaTranslater(deltaX, deltaY);

    Map<GraphNode, Point2D> changes = Point2dUtils.translateNodes(
        getSelectedNodes(), getNodeLocations(), translater);
    editNodeLocations(changes, author);
  }

  /////////////////////////////////////
  // Listeners for drawing metrics

  public void addDrawingListener(DrawingListener listener) {
    drawingListeners.addListener(listener);
  }

  public void removeDrawingListener(DrawingListener listener) {
    drawingListeners.removeListener(listener);
  }

  public void updateDrawingBounds(
      final Rectangle2D drawing, final Rectangle2D viewport) {
    drawingListeners.fireEvent(new ListenerManager.Dispatcher<DrawingListener>() {
      @Override
      public void dispatch(DrawingListener listener) {
        listener.updateDrawingBounds(drawing, viewport);
      }

      @Override
      public void captureException(RuntimeException errAny) {
        logger.warning(errAny.toString());
      }
    });
  }

  /**
   * Capture stable points in the diagram rendering.
   */
  public void sceneChanged() {
    markDirty();
    ScenePreferences prefs = viewInfo.getScenePrefs();
    if (null == prefs) {
      prefs = ScenePreferences.getDefaultScenePrefs();
      viewInfo.setScenePrefs(prefs);
    }

    // Capture the newly stable current camera position in the prefs object.
    renderer.saveCameraPosition(prefs);
  }

  /////////////////////////////////////
  // Notifications from ViewEditor toward Tools and other ViewEditor listeners

  private abstract static class SimpleDispatcher
      implements ListenerManager.Dispatcher<SelectionChangeListener> {

    @Override
    public void captureException(RuntimeException errAny) {
      logger.log(Level.WARNING, "Listener dispatch failure", errAny);
    }
  }

  public void addSelectionChangeListener(SelectionChangeListener listener) {
    selectionListeners.addListener(listener);
  }

  public void removeSelectionChangeListener(SelectionChangeListener listener) {
    selectionListeners.removeListener(listener);
  }

  private void fireExtendSelection(
      final Collection<GraphNode> extension) {
    if (extension.isEmpty()) {
      return;
    }

    selectionListeners.fireEvent(new SimpleDispatcher() {
      @Override
      public void dispatch(SelectionChangeListener listener) {
        listener.extendSelection(extension);
      }
    });
  }

  private void fireReduceSelection(
      final Collection<GraphNode> reduction) {
    if (reduction.isEmpty()) {
      return;
    }

    selectionListeners.fireEvent(new SimpleDispatcher() {
      @Override
      public void dispatch(SelectionChangeListener listener) {
        listener.reduceSelection(reduction);
      }
    });
  }

  private Collection<GraphNode> subtractNodes(
      Collection<GraphNode> from, Collection<GraphNode> minus) {
    if (minus.isEmpty()) {
      return from;
    }
    List<GraphNode> result = Lists.newArrayList(from);
    result.removeAll(minus);
    return result;
  }

  private void updateSelectedNodes(
      Collection<GraphNode> previous,
      Collection<GraphNode> current, Object author) {

    Collection<GraphNode> removeNodes = subtractNodes(previous, current);
    Collection<GraphNode> extendNodes = subtractNodes(current, previous);
    if (author != renderer) {
      renderer.updateSelectedNodes(removeNodes, extendNodes);
    }
    fireReduceSelection(removeNodes);
    fireExtendSelection(extendNodes);
  }

  private void initSelectedNodes(Collection<GraphNode> selection) {
    updateSelectedNodes(GraphNode.EMPTY_NODE_LIST, selection, null);
  }

  /////////////////////////////////////
  // Specialized features

  public HierarchyCache<NodeDisplayProperty> getHierarchies() {
    return hierarchies;
  }

  public GraphData<NodeDisplayProperty> getHierarchy(
      DirectedRelationFinder relFinder) {
    return hierarchies.getHierarchy(relFinder);
  }

  public RelationCount.Settings getRelationCountData() {
    return relationCountData;
  }

  /**
   * Provide the application's Display (i.e. the event loop)
   * @return application's Display
   */
  private static Display getWorkbenchDisplay() {
    return PlatformUI.getWorkbench().getDisplay();
  }

  /**
   * Post an update on the status line.
   * @param statusText text to display on status line
   */
  private void updateStatusLine(final String statusText) {
    getWorkbenchDisplay().asyncExec(new Runnable() {

      @Override
      public void run() {
        getEditorSite().getActionBars().getStatusLineManager()
            .setMessage(statusText);
      }
    });
  }

  public ViewDocument buildNewViewDocument(Collection<GraphNode> nodes) {
    return viewInfo.newViewDocument(nodes);
  }

  /////////////////////////////////////
  // Receiver for notifications from the ViewDocument
  // Most events are mapped to standard re-dispatch methods.

  /**
   * Handle notifications from ViewDocument (mostly UserPreferences) that
   * some user-controlled feature of the view has changed.
   */
  private class Listener implements ViewPrefsListener {
    @Override
    public void edgePropertyChanged(
        GraphEdge edge, EdgeDisplayProperty newProperty) {
      if (null == renderer) {
        return;
      }
      renderer.updateEdgeProperty(edge, newProperty);
      markDirty();
    }

    @Override
    public void relationPropertyChanged(
        Relation relation,  EdgeDisplayProperty newProperty) {
      updateEdgesToRelations();
      markDirty();
    }

    @Override
    public void nodePropertyChanged(
        GraphNode node, NodeDisplayProperty newProperty) {
      if (null == renderer) {
        return;
      }
      renderer.updateNodeProperty(node, newProperty);
      markDirty();
    }

    @Override
    public void collapseChanged(
        Collection<CollapseData> created,
        Collection<CollapseData> removed,
        Object author) {
      updateExposedGraph();
      renderer.updateCollapseChanges(created, removed);
      markDirty();
    }

    @Override
    public void nodeLocationsSet(Map<GraphNode, Point2D> newLocations) {
      renderer.editNodeLocations(newLocations);
      markDirty();
    }

    @Override
    public void nodeLocationsChanged(
        Map<GraphNode, Point2D> newLocations, Object author) {
      // Skip animation if the renderer itself made the move
      if (author == renderer) {
        renderer.updateNodeLocations(newLocations);
      }
      else {
        renderer.editNodeLocations(newLocations);
      }
      markDirty();
    }

    @Override
    public void selectionChanged(Collection<GraphNode> previous,
        Collection<GraphNode> current, Object author) {
      updateSelectedNodes(previous, current, author);
      markDirty();
    }

    @Override
    public void descriptionChanged(String description) {
      // TODO(leeca): update description widget, if it is not the source
      // of the change.
      markDirty();
    }
  }

  /////////////////////////////////////
  // Run the new ViewEditor

  /**
   * Activate a new ViewEditor.
   *
   * This is an asynchronous activate, as the new editor will execute
   * separately from the other workbench windows.
   */
  public static void startViewEditor(ViewDocument newInfo, String baseName) {
    final ViewEditorInput input = new ViewEditorInput(newInfo, baseName);
    getWorkbenchDisplay().asyncExec(new Runnable() {

      @Override
      public void run() {
        IWorkbenchPage page = PlatformUI.getWorkbench()
            .getActiveWorkbenchWindow().getActivePage();
        try {
          page.openEditor(input, ViewEditor.ID);
        } catch (PartInitException e) {
          e.printStackTrace();
        }
      }
    });
  }
}
TOP

Related Classes of com.google.devtools.depan.eclipse.editors.ViewEditor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.