net.schweerelos.parrot.ui.GraphViewComponent.java Source code

Java tutorial

Introduction

Here is the source code for net.schweerelos.parrot.ui.GraphViewComponent.java

Source

/*
 * Copyright (C) 2011 Andrea Schweer
 *
 * This file is part of the Digital Parrot. 
 *
 * The Digital Parrot is free software; you can redistribute it and/or modify
 * it under the terms of the Eclipse Public License as published by the Eclipse
 * Foundation or its Agreement Steward, either version 1.0 of the License, or
 * (at your option) any later version.
 *
 * The Digital Parrot is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the Eclipse Public License for
 * more details.
 *
 * You should have received a copy of the Eclipse Public License along with the
 * Digital Parrot. If not, see http://www.eclipse.org/legal/epl-v10.html. 
 *
 */

package net.schweerelos.parrot.ui;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import net.schweerelos.parrot.model.GraphParrotModel;
import net.schweerelos.parrot.model.NodeWrapper;
import net.schweerelos.parrot.model.ParrotModel;
import net.schweerelos.parrot.model.ParrotModelListener;

import org.apache.commons.collections15.Predicate;
import org.apache.commons.collections15.Transformer;

import edu.uci.ics.jung.algorithms.layout.CircleLayout;
import edu.uci.ics.jung.algorithms.layout.GraphElementAccessor;
import edu.uci.ics.jung.algorithms.layout.KKLayout;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.DirectedSparseMultigraph;
import edu.uci.ics.jung.graph.util.Context;
import edu.uci.ics.jung.visualization.GraphZoomScrollPane;
import edu.uci.ics.jung.visualization.RenderContext;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.control.AbstractPopupGraphMousePlugin;
import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse;
import edu.uci.ics.jung.visualization.control.ModalGraphMouse.Mode;
import edu.uci.ics.jung.visualization.decorators.EdgeShape;
import edu.uci.ics.jung.visualization.decorators.ToStringLabeller;
import edu.uci.ics.jung.visualization.layout.PersistentLayout;
import edu.uci.ics.jung.visualization.picking.PickedInfo;
import edu.uci.ics.jung.visualization.picking.PickedState;
import edu.uci.ics.jung.visualization.renderers.DefaultEdgeLabelRenderer;
import edu.uci.ics.jung.visualization.renderers.DefaultVertexLabelRenderer;
import edu.uci.ics.jung.visualization.renderers.Renderer;
import edu.uci.ics.jung.visualization.renderers.VertexLabelAsShapeRenderer;

public class GraphViewComponent extends JPanel implements MainViewComponent, ParrotStateListener {

    private static final String LAYOUT_SUFFIX = ".layout";

    private static final int PADDING_NODE_BORDER = 12;

    private static final Color COLOR_BACKGROUND = Color.WHITE;

    private static final Color COLOR_NODE_BG = UIConstants.TT_ENVIRONMENT_LIGHT;
    private static final Color COLOR_NODE_PICKED_BG = UIConstants.ACCENT_LIGHT;
    private static final Color COLOR_NODE_HIGHLIGHTED_BG = UIConstants.ENVIRONMENT_LIGHTEST;
    private static final Color COLOR_NODE_WITH_PICKED_NEIGHBOUR_BG = UIConstants.ACCENT_LIGHTEST;
    private static final Color COLOR_NODE_ADJACENT_EDGE_PICKED_BG = COLOR_NODE_WITH_PICKED_NEIGHBOUR_BG;
    private static final Color COLOR_NODE_WITH_HIGHLIGHTED_NEIGHBOUR_BG = UIConstants.T_ENVIRONMENT_LIGHTEST;

    private static final Color COLOR_NODE_BORDER = UIConstants.TT_ENVIRONMENT_MEDIUM;
    private static final Color COLOR_NODE_PICKED_BORDER = UIConstants.ACCENT_MEDIUM;
    private static final Color COLOR_NODE_HIGHLIGHTED_BORDER = UIConstants.ENVIRONMENT_SHADOW_DARK;
    private static final Color COLOR_NODE_WITH_HIGHLIGHTED_NEIGHBOUR_BORDER = COLOR_NODE_HIGHLIGHTED_BORDER;
    private static final Color COLOR_NODE_WITH_PICKED_NEIGHBOUR_BORDER = UIConstants.T_ACCENT_MEDIUM;
    private static final Color COLOR_NODE_ADJACENT_EDGE_PICKED_BORDER = COLOR_NODE_PICKED_BORDER;

    private static final Color COLOR_NODE_TEXT = UIConstants.TT_TEXT;
    private static final Color COLOR_NODE_PICKED_TEXT = UIConstants.TEXT;
    private static final Color COLOR_NODE_HIGHLIGHTED_TEXT = UIConstants.TEXT;
    private static final Color COLOR_NODE_WITH_HIGHLIGHTED_NEIGHBOUR_TEXT = UIConstants.T_TEXT;
    private static final Color COLOR_NODE_WITH_PICKED_NEIGHBOUR_TEXT = COLOR_NODE_WITH_HIGHLIGHTED_NEIGHBOUR_TEXT;
    private static final Color COLOR_NODE_ADJACENT_EDGE_PICKED_TEXT = COLOR_NODE_WITH_HIGHLIGHTED_NEIGHBOUR_TEXT;

    private static final Color COLOR_EDGE = COLOR_NODE_BORDER;
    private static final Color COLOR_EDGE_PICKED = COLOR_NODE_PICKED_BORDER;
    private static final Color COLOR_EDGE_HIGHLIGHTED = COLOR_NODE_HIGHLIGHTED_BORDER;
    private static final Color COLOR_EDGE_ADJACENT_VERTEX_PICKED = COLOR_EDGE_PICKED;
    private static final Color COLOR_EDGE_ADJACENT_VERTEX_HIGHLIGHTED = COLOR_EDGE_HIGHLIGHTED;

    private static final Color COLOR_EDGE_LABEL = UIConstants.TEXT;

    private static final BasicStroke STROKE_EDGE_DEFAULT = new BasicStroke(2);
    private static final BasicStroke STROKE_EDGE_PICKED = new BasicStroke(3);
    private static final BasicStroke STROKE_EDGE_ADJACENT_NODE_PICKED = STROKE_EDGE_PICKED;

    private static final Stroke STROKE_VERTEX_DEFAULT = new BasicStroke(2);
    private static final Stroke STROKE_VERTEX_PICKED = new BasicStroke(3);
    private static final Stroke STROKE_VERTEX_INCOMING_EDGE_PICKED = STROKE_VERTEX_PICKED;
    private static final Stroke STROKE_VERTEX_OUTGOING_EDGE_PICKED = STROKE_VERTEX_DEFAULT;
    private static final Stroke STROKE_VERTEX_HIGHLIGHTED = STROKE_VERTEX_PICKED;

    private static final long serialVersionUID = 1L;

    static final Icon MOVING_ICON = new ImageIcon("images/graph-move.png");
    static final Icon SELECTING_ICON = new ImageIcon("images/graph-select.png");

    private static final GridBagConstraints CONTENT_CONSTRAINTS = new GridBagConstraints();

    static {
        CONTENT_CONSTRAINTS.fill = GridBagConstraints.BOTH;
        CONTENT_CONSTRAINTS.weightx = 1;
        CONTENT_CONSTRAINTS.weighty = 1;
    }

    private VisualizationViewer<NodeWrapper, NodeWrapper> vv;
    private Layout<NodeWrapper, NodeWrapper> layout;
    private Graph<NodeWrapper, NodeWrapper> graph;
    private ParrotModel model;
    private NodeWrapperPopupMenu popup;
    private IncludePredicate<Context<Graph<NodeWrapper, NodeWrapper>, NodeWrapper>> includePredicate;

    private DoubleClickPickingModalGraphMouse<NodeWrapper, NodeWrapper> mouse;

    private JComponent view;

    private List<PickListener> pickListeners = new ArrayList<PickListener>();

    public GraphViewComponent() {
        super();
        setLayout(new GridBagLayout());
        setBorder(
                BorderFactory.createCompoundBorder(
                        BorderFactory.createEmptyBorder(PADDING_NODE_BORDER, PADDING_NODE_BORDER,
                                PADDING_NODE_BORDER, PADDING_NODE_BORDER),
                        BorderFactory.createLoweredBevelBorder()));

        layout = new NodeWrapperPersistentLayoutImpl(new CircleLayout<NodeWrapper, NodeWrapper>(
                new DirectedSparseMultigraph<NodeWrapper, NodeWrapper>()));
        vv = new VisualizationViewer<NodeWrapper, NodeWrapper>(layout);

        setupRenderContext(vv);

        view = new JScrollPane(vv);
        add(view, CONTENT_CONSTRAINTS);
    }

    @SuppressWarnings("serial")
    private void setupRenderContext(final VisualizationViewer<NodeWrapper, NodeWrapper> vis) {
        vis.setRenderer(new ParrotGraphRenderer());
        vis.setPickSupport(new ParrotPickSupport(vis));

        RenderContext<NodeWrapper, NodeWrapper> renderContext = vis.getRenderContext();

        final PickedInfo<NodeWrapper> vertexPickInfo = vis.getPickedVertexState();
        final PickedState<NodeWrapper> edgePickInfo = vis.getPickedEdgeState();

        // hide all edge arrows except for those on outgoing edges of picked
        // nodes
        renderContext.setEdgeArrowPredicate(new Predicate<Context<Graph<NodeWrapper, NodeWrapper>, NodeWrapper>>() {
            @Override
            public boolean evaluate(Context<Graph<NodeWrapper, NodeWrapper>, NodeWrapper> context) {
                NodeWrapper edge = context.element;
                NodeWrapper source = graph.getSource(edge);
                return vertexPickInfo.isPicked(source);
            }
        });

        // make edges straight lines to collapse parallel edges
        renderContext.setEdgeShapeTransformer(new EdgeShape.Line<NodeWrapper, NodeWrapper>());

        // hide text of all edges except for outgoing edges of picked nodes
        renderContext.setEdgeLabelTransformer(new Transformer<NodeWrapper, String>() {
            @Override
            public String transform(NodeWrapper edge) {
                NodeWrapper source = graph.getSource(edge);
                NodeWrapper destination = graph.getDest(edge);
                if (vertexPickInfo.isPicked(source) && !vertexPickInfo.isPicked(destination)) {
                    return edge.toString();
                } else {
                    return "";
                }
            }
        });
        renderContext.setEdgeLabelRenderer(new DefaultEdgeLabelRenderer(COLOR_EDGE_LABEL) {
            @Override
            public <E> Component getEdgeLabelRendererComponent(JComponent vv, Object value, Font font,
                    boolean isSelected, E edge) {
                Component component = super.getEdgeLabelRendererComponent(vv, value, font, isSelected, edge);
                component.setForeground(COLOR_EDGE_LABEL);
                return component;
            }

        });

        // start from VertexLabelAsShapeDemo

        // this class will provide both label drawing and vertex shapes
        VertexLabelAsShapeRenderer<NodeWrapper, NodeWrapper> vlasr = new VertexLabelAsShapeRenderer<NodeWrapper, NodeWrapper>(
                renderContext);
        renderContext.setVertexShapeTransformer(vlasr);

        vis.setForeground(COLOR_NODE_TEXT);

        // customize the render context

        renderContext.setVertexLabelTransformer(new ToStringLabeller<NodeWrapper>());

        renderContext.setVertexLabelRenderer(new DefaultVertexLabelRenderer(COLOR_NODE_PICKED_TEXT) {
            @Override
            public <V> Component getVertexLabelRendererComponent(JComponent vv, Object value, Font font,
                    boolean isSelected, V vertexToRender) {
                Component component = super.getVertexLabelRendererComponent(vv, value, font, isSelected,
                        vertexToRender);
                if (component instanceof JLabel) {
                    JLabel label = (JLabel) component;
                    // add a little bit of padding around the text
                    Border originalBorder = label.getBorder();
                    label.setBorder(BorderFactory.createCompoundBorder(originalBorder,
                            BorderFactory.createEmptyBorder(3, 2, 4, 2)));
                }
                // now set the colour/font too
                if (vertexToRender instanceof NodeWrapper) {
                    NodeWrapper vertex = (NodeWrapper) vertexToRender;
                    if (vertexPickInfo.isPicked(vertex)) {
                        component.setForeground(COLOR_NODE_PICKED_TEXT);
                    } else if (vertex.isHighlighted()) {
                        component.setForeground(COLOR_NODE_HIGHLIGHTED_TEXT);
                        component.setFont(font.deriveFont(Font.BOLD));
                    } else if (GraphViewHelper.hasPickedNeighbour(vertex, vertexPickInfo, graph)) {
                        component.setForeground(COLOR_NODE_WITH_PICKED_NEIGHBOUR_TEXT);
                    } else if (GraphViewHelper.hasPickedAdjacentEdge(vertex, edgePickInfo, graph)) {
                        component.setForeground(COLOR_NODE_ADJACENT_EDGE_PICKED_TEXT);
                    } else if (GraphViewHelper.hasHighlightedNeighbour(vertex, graph)) {
                        component.setForeground(COLOR_NODE_WITH_HIGHLIGHTED_NEIGHBOUR_TEXT);
                    } else {
                        component.setForeground(COLOR_NODE_TEXT);
                    }
                }

                return component;
            }
        });

        // end from VertexLabelAsShapeDemo

        vis.getRenderer().getVertexLabelRenderer().setPosition(Renderer.VertexLabel.Position.CNTR);

        vis.setVertexToolTipTransformer(new Transformer<NodeWrapper, String>() {
            @Override
            public String transform(NodeWrapper vertex) {
                return vertex.getToolTipText(model);
            }
        });

        // inspired by PluggableRendererDemo
        Transformer<NodeWrapper, Paint> vertexOutline = new Transformer<NodeWrapper, Paint>() {
            @Override
            public Paint transform(NodeWrapper vertex) {
                if (vertexPickInfo.isPicked(vertex)) {
                    return COLOR_NODE_PICKED_BORDER;
                } else if (vertex.isHighlighted()) {
                    return COLOR_NODE_HIGHLIGHTED_BORDER;
                } else {
                    if (GraphViewHelper.hasPickedAdjacentEdge(vertex, edgePickInfo, graph)) {
                        return COLOR_NODE_ADJACENT_EDGE_PICKED_BORDER;
                    }
                    if (GraphViewHelper.hasPickedNeighbour(vertex, vertexPickInfo, graph)) {
                        return COLOR_NODE_WITH_PICKED_NEIGHBOUR_BORDER;
                    } else if (GraphViewHelper.hasHighlightedNeighbour(vertex, graph)) {
                        return COLOR_NODE_WITH_HIGHLIGHTED_NEIGHBOUR_BORDER;
                    }
                    // will get here only if no neighbour is picked/highlighted
                    return COLOR_NODE_BORDER;
                }
            }
        };
        renderContext.setVertexDrawPaintTransformer(vertexOutline);

        Transformer<NodeWrapper, Paint> vertexBackground = new Transformer<NodeWrapper, Paint>() {
            @Override
            public Paint transform(NodeWrapper vertex) {
                if (vertexPickInfo.isPicked(vertex)) {
                    return COLOR_NODE_PICKED_BG;
                } else if (vertex.isHighlighted()) {
                    return COLOR_NODE_HIGHLIGHTED_BG;
                } else {
                    if (GraphViewHelper.hasPickedAdjacentEdge(vertex, edgePickInfo, graph)) {
                        return COLOR_NODE_ADJACENT_EDGE_PICKED_BG;
                    }
                    if (GraphViewHelper.hasPickedNeighbour(vertex, vertexPickInfo, graph)) {
                        return COLOR_NODE_WITH_PICKED_NEIGHBOUR_BG;
                    } else if (GraphViewHelper.hasHighlightedNeighbour(vertex, graph)) {
                        return COLOR_NODE_WITH_HIGHLIGHTED_NEIGHBOUR_BG;
                    }
                    return COLOR_NODE_BG;
                }
            }
        };
        renderContext.setVertexFillPaintTransformer(vertexBackground);

        Transformer<NodeWrapper, Stroke> vertexStroke = new Transformer<NodeWrapper, Stroke>() {
            @Override
            public Stroke transform(NodeWrapper vertex) {
                if (vertexPickInfo.isPicked(vertex)) {
                    return STROKE_VERTEX_PICKED;
                } else if (vertex.isHighlighted()) {
                    return STROKE_VERTEX_HIGHLIGHTED;
                }

                Collection<NodeWrapper> edges = graph.getInEdges(vertex);
                for (NodeWrapper edge : edges) {
                    if (edgePickInfo.isPicked(edge)) {
                        return STROKE_VERTEX_INCOMING_EDGE_PICKED;
                    }
                }
                edges = graph.getOutEdges(vertex);
                for (NodeWrapper edge : edges) {
                    if (edgePickInfo.isPicked(edge)) {
                        return STROKE_VERTEX_OUTGOING_EDGE_PICKED;
                    }
                }

                // we'll only get here if none of the cases above applies
                return STROKE_VERTEX_DEFAULT;
            }
        };
        renderContext.setVertexStrokeTransformer(vertexStroke);

        Transformer<NodeWrapper, Stroke> edgeStroke = new Transformer<NodeWrapper, Stroke>() {
            @Override
            public Stroke transform(NodeWrapper edge) {
                NodeWrapper edgeSource = graph.getSource(edge);
                if (edgePickInfo.isPicked(edge)) {
                    return STROKE_EDGE_PICKED;
                } else if (vertexPickInfo.isPicked(edgeSource)) {
                    return STROKE_EDGE_ADJACENT_NODE_PICKED;
                } else {
                    return STROKE_EDGE_DEFAULT;
                }
            }
        };
        renderContext.setEdgeStrokeTransformer(edgeStroke);

        Transformer<NodeWrapper, Paint> edgeColor = new Transformer<NodeWrapper, Paint>() {
            @Override
            public Paint transform(NodeWrapper edge) {
                if (edgePickInfo.isPicked(edge)) {
                    return COLOR_EDGE_PICKED;
                } else if (GraphViewHelper.hasPickedAdjacentVertex(edge, vertexPickInfo, graph)) {
                    return COLOR_EDGE_ADJACENT_VERTEX_PICKED;
                } else if (edge.isHighlighted()) {
                    return COLOR_EDGE_HIGHLIGHTED;
                } else if (GraphViewHelper.hasHighlightedAdjacentVertex(edge, graph)) {
                    return COLOR_EDGE_ADJACENT_VERTEX_HIGHLIGHTED;
                } else {
                    return COLOR_EDGE;
                }
            }
        };
        renderContext.setEdgeDrawPaintTransformer(edgeColor);
        // draw arrows in the same colour as edges
        renderContext.setArrowDrawPaintTransformer(edgeColor);
        renderContext.setArrowFillPaintTransformer(edgeColor);

        includePredicate = new IncludePredicate<Context<Graph<NodeWrapper, NodeWrapper>, NodeWrapper>>();
        renderContext.setEdgeIncludePredicate(includePredicate);
        renderContext.setVertexIncludePredicate(includePredicate);

        vis.setBackground(COLOR_BACKGROUND);

        mouse = new DoubleClickPickingModalGraphMouse<NodeWrapper, NodeWrapper>();
        mouse.add(new AbstractPopupGraphMousePlugin() {
            @Override
            protected void handlePopup(MouseEvent e) {
                if (!e.isPopupTrigger()) {
                    return;
                }
                GraphElementAccessor<NodeWrapper, NodeWrapper> pickSupport = vis.getPickSupport();
                if (pickSupport == null) {
                    return;
                }

                NodeWrapper node = pickSupport.getVertex(layout, e.getX(), e.getY());
                if (node == null) {
                    node = pickSupport.getEdge(layout, e.getX(), e.getY());
                }
                if (node == null) {
                    return;
                }
                popup.setNodeWrapper(node);
                popup.show(vis, e.getX(), e.getY());
            }
        });
        mouse.setDoubleClickPickingPlugin(new DoubleClickPickingPlugin() {
            @Override
            void doubleClickOccurred(MouseEvent e) {
                GraphElementAccessor<NodeWrapper, NodeWrapper> pickSupport = vis.getPickSupport();
                if (pickSupport == null) {
                    return;
                }

                NodeWrapper node = pickSupport.getVertex(layout, e.getX(), e.getY());
                if (node == null) {
                    return;
                }
                fireNodeSelected(node);
            }
        });
        vis.setGraphMouse(mouse);

    }

    @Override
    public void setModel(ParrotModel model) {
        removeAll();

        if (model == null) {
            maybeSaveLayout();
            layout = null;
            vv.setGraphLayout(null);
            return;
        }

        if (!(model instanceof GraphParrotModel)) {
            throw new IllegalArgumentException("model must be a graph model");
        }

        this.model = model;
        graph = ((GraphParrotModel) model).asGraph();
        popup = new NodeWrapperPopupMenu(SwingUtilities.getRoot(this), model);

        model.addParrotModelListener(new ParrotModelListener() {
            @Override
            public void highlightsChanged() {
                SwingUtilities.invokeLater(new Runnable() {

                    @Override
                    public void run() {
                        vv.fireStateChanged();
                        vv.repaint();
                    }
                });
            }

            @Override
            public void restrictionsChanged(final Collection<NodeWrapper> currentlyHidden) {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        includePredicate.setCurrentlyHidden(currentlyHidden);
                        vv.repaint();
                    }
                });
            }

            @Override
            public void modelBusy() {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        synchronized (GraphViewComponent.this.model) {
                            if (!GraphViewComponent.this.model.isBusy()) {
                                return;
                            }
                            vv.setEnabled(false);
                            view.setEnabled(false);
                            GraphViewComponent.this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
                            view.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
                            vv.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
                        }
                    }
                });
            }

            @Override
            public void modelIdle() {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        synchronized (GraphViewComponent.this.model) {
                            if (GraphViewComponent.this.model.isBusy()) {
                                return;
                            }
                            view.setEnabled(true);
                            vv.setEnabled(true);
                            GraphViewComponent.this.setCursor(Cursor.getDefaultCursor());
                            view.setCursor(Cursor.getDefaultCursor());
                            vv.setCursor(Cursor.getDefaultCursor());
                        }
                    }
                });
            }
        });

        layout = new NodeWrapperPersistentLayoutImpl(new KKLayout<NodeWrapper, NodeWrapper>(graph));
        layout.setSize(new Dimension(880, 600));
        String layoutFilename = getLayoutFilename();
        try {
            if (new File(layoutFilename).canRead()) {
                ((PersistentLayout<NodeWrapper, NodeWrapper>) layout).restore(layoutFilename);
            }
        } catch (IOException e) {
            // TODO #1 Auto-generated catch block
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            // TODO #1 Auto-generated catch block
            e.printStackTrace();
        }

        vv.setGraphLayout(layout);
        GraphZoomScrollPane pane = new GraphZoomScrollPane(vv);
        ModeToggle modeToggle = new ModeToggle(mouse);
        pane.setCorner(modeToggle);
        add(pane, CONTENT_CONSTRAINTS);
        view = pane;
    }

    @Override
    public Collection<NodeWrapper> getSelectedNodes() {
        return vv.getPickedVertexState().getPicked();
    }

    private String getLayoutFilename() {
        String dataID = model.getDataIdentifier();
        int lastSlashIndex = dataID.lastIndexOf(File.separatorChar);
        int lastDotIndex = dataID.lastIndexOf('.');
        String filename;
        if (lastSlashIndex < lastDotIndex) {
            // this is what we prefer
            filename = dataID.substring(lastSlashIndex, lastDotIndex);
        } else {
            filename = new StringBuilder(dataID.hashCode()).toString();
        }
        return System.getProperty("user.home") + File.separator + ".digital-parrot" + File.separator + filename
                + LAYOUT_SUFFIX;
    }

    private void maybeSaveLayout() {
        if (layout instanceof PersistentLayout) {
            PersistentLayout<NodeWrapper, NodeWrapper> theLayout = (PersistentLayout<NodeWrapper, NodeWrapper>) layout;
            try {
                String filename = getLayoutFilename();
                File directory = new File(filename).getParentFile();
                if (!directory.canWrite()) {
                    directory.mkdirs();
                }
                theLayout.persist(filename);
            } catch (IOException e) {
                // TODO #1 Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    @Override
    public JComponent asJComponent() {
        return this;
    }

    @Override
    public void parrotExiting() {
        maybeSaveLayout();
    }

    @SuppressWarnings("serial")
    class ModeToggle extends JToggleButton {

        ModeToggle(final DefaultModalGraphMouse<NodeWrapper, NodeWrapper> mouse) {
            setIcon(MOVING_ICON);
            setSelectedIcon(SELECTING_ICON);
            setText("");
            setToolTipText(isSelected() ? "Make mouse move graph" : "Make mouse select");
            mouse.addItemListener(new ItemListener() {
                @Override
                public void itemStateChanged(ItemEvent e) {
                    if (e.getStateChange() == ItemEvent.SELECTED) {
                        setSelected(e.getItem() == Mode.PICKING);
                        setText("");
                        setToolTipText(isSelected() ? "Make mouse move graph" : "Make mouse select");
                    }
                }
            });
            addChangeListener(new ChangeListener() {
                @Override
                public void stateChanged(ChangeEvent e) {
                    if (isSelected()) {
                        mouse.setMode(Mode.PICKING);
                    } else {
                        mouse.setMode(Mode.TRANSFORMING);
                    }
                }
            });
            setSelected(true);
        }

    }

    class IncludePredicate<T> implements Predicate<Context<Graph<NodeWrapper, NodeWrapper>, NodeWrapper>> {

        private Collection<NodeWrapper> currentlyHidden;

        @Override
        public boolean evaluate(Context<Graph<NodeWrapper, NodeWrapper>, NodeWrapper> context) {
            if (currentlyHidden == null) {
                return true;
            }
            NodeWrapper element = context.element;
            return !currentlyHidden.contains(element);
        }

        void setCurrentlyHidden(Collection<NodeWrapper> currentlyHidden) {
            this.currentlyHidden = currentlyHidden;
        }

    }

    private void fireNodeSelected(NodeWrapper newSelection) {
        List<PickListener> listeners;
        synchronized (this) {
            listeners = Collections.synchronizedList(pickListeners);
        }
        synchronized (listeners) {
            for (PickListener listener : listeners) {
                try {
                    listener.picked(newSelection);
                } catch (RuntimeException re) {
                    re.printStackTrace();
                    pickListeners.remove(listener);
                }
            }
        }
    }

    @Override
    public void addPickListener(PickListener listener) {
        pickListeners.add(listener);
    }

    @Override
    public void removePickListener(PickListener listener) {
        pickListeners.remove(listener);
    }

    @Override
    public String getTitle() {
        return "Graph";
    }

}