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