package nodebox.client;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import nodebox.util.IOrderedFields;
import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import java.awt.*;
import java.util.List;
import java.util.Map;
import static com.google.common.base.Preconditions.checkArgument;
/**
* The Data Sheet presents data in a spreadsheet view.
*/
public class DataSheet extends JPanel implements OutputView {
private final DataTableModel tableModel;
private final JTable table;
public DataSheet() {
super(new BorderLayout());
table = new DataTable();
table.setAutoCreateRowSorter(true);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
table.addColumn(new TableColumn(0));
tableModel = new DataTableModel();
table.setModel(tableModel);
JScrollPane tableScroll = new JScrollPane(table);
tableScroll.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
add(tableScroll, BorderLayout.CENTER);
}
public void setOutputValues(List<?> objects) {
tableModel.setOutputValues(objects);
table.setModel(tableModel);
}
// Optimization techniques based on "Christmas Tree" article:
// http://java.sun.com/products/jfc/tsc/articles/ChristmasTree/
private final class DataTable extends JTable {
private final DataCellRenderer cellRenderer = new DataCellRenderer();
@Override
public TableCellRenderer getCellRenderer(int row, int column) {
// Always return the same object for the whole table.
return cellRenderer;
}
}
private final class DataCellRenderer extends DefaultTableCellRenderer {
private Color zebraColor = UIManager.getColor("Table.alternateRowColor");
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int rowIndex, int columnIndex) {
setValue(value);
if (rowIndex % 2 == 0) {
setBackground(null);
} else {
setBackground(zebraColor);
}
return this;
}
@Override
public void paint(Graphics g) {
ui.update(g, this);
}
@Override
public boolean isOpaque() {
return getBackground() != null;
}
@Override
public void invalidate() {
// This is generally ok for non-Composite components (like Labels)
}
@Override
public void repaint() {
// Can be ignored, we don't exist in the containment hierarchy.
}
}
private final class DataTableModel extends AbstractTableModel {
public static final int MAX_VALUE_LENGTH = 100;
private List<?> outputValues = ImmutableList.of();
private List<String> keys = ImmutableList.of();
public void setOutputValues(List<?> outputValues) {
if (outputValues == null) {
this.outputValues = ImmutableList.of();
} else {
this.outputValues = outputValues;
}
if (this.outputValues.size() == 0) {
keys = ImmutableList.of();
} else {
keys = getColumnNames(this.outputValues.get(0));
}
fireTableChanged(new TableModelEvent(this, TableModelEvent.ALL_COLUMNS));
}
/**
* Inspect the object and return a list of column names.
* <p/>
* If the object is already a map, return the keys.
* <p/>
* If the object is something else, return it as a {"Data": o.toString()}
*
* @param o The object to inspect
* @return a Map.
*/
private List<String> getColumnNames(Object o) {
if (o instanceof IOrderedFields) {
return ImmutableList.copyOf(((IOrderedFields)o).getOrderedFields());
} else if (o instanceof Map) {
ImmutableList.Builder<String> b = ImmutableList.builder();
for (Object k : ((Map) o).keySet()) {
b.add(k.toString());
}
return b.build();
} else {
return ImmutableList.of("Data");
}
}
private Map inspect(Object o) {
if (o instanceof Map) {
return (Map) o;
} else {
return ImmutableMap.of("Data", o);
}
}
public int getRowCount() {
return outputValues.size();
}
public int getColumnCount() {
return keys.size() + 1;
}
public Object getValueAt(int rowIndex, int columnIndex) {
checkArgument(rowIndex < outputValues.size(), "The row index %s is larger than the number of values.", rowIndex);
checkArgument(columnIndex < keys.size() + 1, "The column index %s is larger than the number of columns.", columnIndex);
Map o = inspect(outputValues.get(rowIndex));
if (columnIndex > o.size()) {
return "<not found>";
} else if (columnIndex == 0) {
return rowIndex;
} else {
String key = keys.get(columnIndex - 1);
return objectToString(o.get(key));
}
}
public String objectToString(Object o) {
String s = o == null ? "<null>" : o.toString();
if (s.length() <= MAX_VALUE_LENGTH) {
return s;
} else {
return s.substring(0, MAX_VALUE_LENGTH) + "...";
}
}
@Override
public String getColumnName(int columnIndex) {
if (columnIndex == 0) {
return "Index";
} else {
return keys.get(columnIndex - 1);
}
}
}
}