/* * CCVisu is a tool for visual graph clustering * and general force-directed graph layout. * This file is part of CCVisu. * * Copyright (C) 2005-2012 Dirk Beyer * * CCVisu is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * CCVisu 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with CCVisu; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Please find the GNU Lesser General Public License in file * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt * * Dirk Beyer (firstname.lastname@uni-passau.de) * University of Passau, Bavaria, Germany */ package org.sosy_lab.ccvisu.ui.controlpanel; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkElementIndex; import static com.google.common.base.Preconditions.checkNotNull; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Arrays; import java.util.EventObject; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.AbstractCellEditor; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JColorChooser; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.RowFilter; import javax.swing.ScrollPaneConstants; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import javax.swing.border.TitledBorder; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableRowSorter; import org.sosy_lab.ccvisu.graph.GraphData; import org.sosy_lab.ccvisu.graph.GraphVertex; import org.sosy_lab.ccvisu.graph.GraphVertex.Shape; import org.sosy_lab.ccvisu.graph.Group; import org.sosy_lab.ccvisu.graph.NameVisibility.Priority; import org.sosy_lab.ccvisu.graph.interfaces.GraphEventListener; import org.sosy_lab.ccvisu.ui.helper.ColorComboBox; import org.sosy_lab.ccvisu.ui.helper.ShapeComboBox; import org.sosy_lab.ccvisu.writers.WriterDataLayoutDISP; import org.sosy_lab.util.Colors; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; @SuppressWarnings("serial") public class ToolPanelNodeManager extends ControlPanel implements GraphEventListener { /** The column headings. */ private static String[] COLUMN_HEADINGS = { "", "Group", "Label", "InDegree", "OutDegree" }; /** The column which contains the vertex' label. */ private final static int COLUMN_LABEL = 2; /** The default width of the columns. */ private static Integer[] COLUMNS_WIDTH = { 1, 50, 300, 25, 25 }; /** The table itself. */ private final JTable table = new JTable(); /** The model of the table. */ private final GraphVertexTableModel tableModel = new GraphVertexTableModel(); /** The text field for the filter. */ private final JTextField filterRegex = new JTextField(); /** The row sorter of the table.*/ private TableRowSorter<GraphVertexTableModel> sorter; /** Handle to the writer */ private final WriterDataLayoutDISP writer; /** The graph */ private final GraphData graph; /** * Creates a new control panel containing the node manager. * * @param ccvisuOptions the global options */ public ToolPanelNodeManager(WriterDataLayoutDISP writer, GraphData graph) { Preconditions.checkNotNull(graph); this.graph = graph; this.writer = writer; // listen to graph and group changes (to update the table) graph.addOnGraphChangeListener(this); graph.addOnGroupChangeListener(this); // set layout and add table and filter setLayout(new BorderLayout(5, 5)); final JComponent tablePane = createTableComponent(); add(tablePane, BorderLayout.CENTER); final JPanel filterPanel = new JPanel(); filterPanel.setLayout(new BorderLayout()); final JComponent filterArea = createFilterComponent(); final JComponent applyToFilteredComponent = createApplyToNodesComponent(); filterPanel.add(filterArea, BorderLayout.NORTH); filterPanel.add(applyToFilteredComponent, BorderLayout.CENTER); add(filterPanel, BorderLayout.SOUTH); } /** * Informs the table about changes of the graph and groups. */ @Override public void onGraphChangedEvent(EventObject event) { if (graph == null || graph.getVertices() == null) { return; } List<GraphVertex> vertices = graph.getVertices(); synchronized (vertices) { // update table model if (tableModel != null) { tableModel.setData(vertices); } } } /** * Creates a new component containing the table. * @return the component */ private final JComponent createTableComponent() { // set the tables properties table.setDefaultRenderer(Color.class, new ColorTableCellRenderer()); table.setDefaultEditor(Color.class, new ColorEditor()); // fill the table with the graph's vertices List<GraphVertex> vertices = graph.getVertices(); synchronized (vertices) { tableModel.setData(vertices); } table.setModel(tableModel); // set the columns default width for (int i = 0; i < COLUMN_HEADINGS.length; i++) { TableColumn column = table.getColumnModel().getColumn(i); column.setPreferredWidth(COLUMNS_WIDTH[i]); } // create table pane JScrollPane tablePane = new JScrollPane(table); int asNeeded = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED; tablePane.setHorizontalScrollBarPolicy(asNeeded); asNeeded = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED; tablePane.setVerticalScrollBarPolicy(asNeeded); return tablePane; } /** * Creates a new component containing the filter field. * @return the component */ private final JComponent createFilterComponent() { JPanel form = new JPanel(); form.setLayout(new GridBagLayout()); // add the filter regex field and a corresponding label addOptionControls(form, "Filter Text: ", filterRegex); // create a new filter every time the content of the regex field changes filterRegex.getDocument().addDocumentListener(new DocumentListener() { @Override public void changedUpdate(DocumentEvent event) { updateFilter(); } @Override public void insertUpdate(DocumentEvent event) { updateFilter(); } @Override public void removeUpdate(DocumentEvent event) { updateFilter(); } private void updateFilter() { if (sorter == null) { return; } if (tableModel.getRowCount() < 1) { sorter.setRowFilter(null); return; } RowFilter<GraphVertexTableModel, Object> rowFilter = null; try { rowFilter = RowFilter.regexFilter(filterRegex.getText(), COLUMN_LABEL); } catch (java.util.regex.PatternSyntaxException e) { return; } sorter.setRowFilter(rowFilter); } }); return form; } private final JComponent createApplyToNodesComponent() { JPanel form = new JPanel(); form.setLayout(new GridBagLayout()); final ShapeComboBox shapeComboBox = new ShapeComboBox(Shape.DISC); final ColorComboBox colorComboBox = new ColorComboBox(Colors.GREEN.get()); shapeComboBox.setColor(colorComboBox.getSelectedColor()); colorComboBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { shapeComboBox.setColor(colorComboBox.getSelectedColor()); shapeComboBox.repaint(); } }); final JCheckBox showLabelCheckBox = new JCheckBox("Yes"); final JCheckBox nodeVisibleCheckBox = new JCheckBox("Yes"); // defaults showLabelCheckBox.setSelected(false); nodeVisibleCheckBox.setSelected(true); JButton applyToNodesButton = new JButton("Apply to filtered nodes!"); addOptionControls(form, "Node visible?", nodeVisibleCheckBox); addOptionControls(form, "Show labels?", showLabelCheckBox); addOptionControls(form, "Color: ", colorComboBox); colorComboBox.setToolTipText("The color of the filtered nodes."); addOptionControls(form, "Shape: ", shapeComboBox); shapeComboBox.setToolTipText("The shape of the filtered nodes."); addOptionControls(form, "", applyToNodesButton); applyToNodesButton.setToolTipText("Apply the selected attributes to all filtered nodes."); applyToNodesButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (sorter == null) { return; } for (int rowIndex = 0; rowIndex < sorter.getViewRowCount(); rowIndex++) { int modelRow = sorter.convertRowIndexToModel(rowIndex); int vertexId = tableModel.getVertexId(modelRow); GraphVertex vertex = graph.getVertexById(vertexId); vertex.setColor(colorComboBox.getSelectedColor()); vertex.setShowVertex(nodeVisibleCheckBox.isSelected()); vertex.setShape(shapeComboBox.getSelectedShape()); vertex.setShowName(Priority.FIND, showLabelCheckBox.isSelected()); } graph.notifyAboutLayoutChange(new EventObject(this)); writer.getDisplay().getCanvas().updateAndPaint(); } }); TitledBorder border = BorderFactory.createTitledBorder("Set attributes of filtered nodes."); form.setBorder(new CompoundBorder(new EmptyBorder(10, 10, 10, 10), border)); return form; } /** * The table model for vertices. */ private class GraphVertexTableModel extends AbstractTableModel { private Object[][] data; /** Maps the index of a vertex in the object table to the id of the vertex. */ private Map<Integer, Integer> indexToIdMap; /** The column that contains the color. */ private static final int COLUMN_COLOR = 0; public GraphVertexTableModel() { data = new Object[0][0]; indexToIdMap = Maps.newHashMap(); } @Override public int getRowCount() { return data.length; } @Override public int getColumnCount() { return COLUMN_HEADINGS.length; } @Override public String getColumnName(int column) { checkElementIndex(column, getColumnCount()); return COLUMN_HEADINGS[column]; } @Override public Object getValueAt(int rowIndex, int columnIndex) { checkElementIndex(rowIndex, getRowCount()); checkElementIndex(columnIndex, getColumnCount()); return data[rowIndex][columnIndex]; } public int getVertexId(int row) { checkArgument(row >= 0 || row < data.length, "Illegal parameter range."); return indexToIdMap.get(row); } public void setData(List<GraphVertex> vertices) { setData(formatTableData(vertices)); } private void setData(Object[][] data) { checkNotNull(data); for (Object[] row : data) { checkNotNull(row, "parameter may not contain null rows."); for (Object cell : row) { checkNotNull(cell, "parameter may not contain null columns."); } } // test if update is necessary if (Arrays.deepEquals(this.data, data)) { // the model will not be changed. The effect is that marked vertices // are still marked when only the layout, not the graph has changed. return; } // prepare update int oldRowCount = this.data.length; int newRowCount = data.length; // update this.data = data; // inform about update if (newRowCount > oldRowCount) { // there are new rows fireTableRowsInserted(oldRowCount, newRowCount - 1); // the old ones may have been updated, too fireTableRowsUpdated(0, Math.max(0, oldRowCount - 1)); } else if (newRowCount < oldRowCount && newRowCount > 0) { // some rows were deleted fireTableRowsDeleted(newRowCount, oldRowCount); // the remaining may have been updated, too fireTableRowsUpdated(0, Math.max(0, newRowCount - 1)); } else if (newRowCount > 0) { // there are rows, and parts/all of them were updated fireTableRowsUpdated(0, Math.max(0, newRowCount - 1)); } else { // there were or there are no rows } if (newRowCount > 0) { if (sorter == null) { sorter = new TableRowSorter<GraphVertexTableModel>(tableModel); sorter.setSortsOnUpdates(true); } filterRegex.setEditable(true); filterRegex.setFocusable(true); } else { sorter = null; filterRegex.setEditable(false); filterRegex.setFocusable(false); } table.setRowSorter(sorter); } @Override public Class<?> getColumnClass(int column) { return getValueAt(0, column).getClass(); } @Override public boolean isCellEditable(int row, int col) { if (col == COLUMN_COLOR && row >= 0) { return true; } return false; } private Object[][] formatTableData(List<GraphVertex> vertices) { checkNotNull(vertices); Object[][] tableData = new Object[0][0]; indexToIdMap = new HashMap<Integer, Integer>(vertices.size()); synchronized (vertices) { int i = 0; tableData = new Object[vertices.size()][COLUMN_HEADINGS.length]; for (GraphVertex vertex : vertices) { if (!vertex.isAuxiliary()) { tableData[i][COLUMN_COLOR] = vertex.getColor(); Group group = graph.findGroupOfVertex(vertex); if (group != null) { tableData[i][1] = group.getName(); } else { tableData[i][1] = "unknown"; } tableData[i][2] = vertex.getLabel(); tableData[i][3] = vertex.getDegree(); tableData[i][4] = vertex.getDegreeOut(); indexToIdMap.put(i, vertex.getId()); ++i; } } } return tableData; } } /** * A cell renderer for cells containing an object of type color. */ private class ColorTableCellRenderer extends JLabel implements TableCellRenderer { public ColorTableCellRenderer() { setOpaque(true); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Color color = (Color) value; setBackground(color); setText(""); setToolTipText(Colors.toString(color)); return this; } } /** * ColorEditor allows to change the color of a vertex in the Node Manager. */ private class ColorEditor extends AbstractCellEditor implements TableCellEditor, ActionListener { private Color currentColor; private int currentRow; private JButton button; private JColorChooser colorChooser; private JDialog dialog; private static final String EDIT = "edit"; public ColorEditor() { button = new JButton(); button.setActionCommand(EDIT); button.addActionListener(this); button.setBorderPainted(false); //Set up the dialog that the button brings up. colorChooser = new JColorChooser(); dialog = JColorChooser.createDialog(button, "Select Color", true, colorChooser, this, null); } @Override public void actionPerformed(ActionEvent e) { if (EDIT.equals(e.getActionCommand())) { // The user has clicked the cell, so bring up the dialog. button.setBackground(currentColor); colorChooser.setColor(currentColor); dialog.setVisible(true); // Make the renderer reappear. fireEditingStopped(); } else { // User pressed dialog's "OK" button. int row = sorter.convertRowIndexToModel(currentRow); int currentId = tableModel.getVertexId(row); GraphVertex vertex = graph.getVertexById(currentId); vertex.setColor(colorChooser.getColor()); graph.notifyAboutLayoutChange(new EventObject(this)); currentColor = colorChooser.getColor(); } } @Override public Object getCellEditorValue() { return currentColor; } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { currentRow = row; currentColor = (Color)value; return button; } } }