/* * 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"; } }