package org.phenoscape.view;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.DefaultCellEditor;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.basic.BasicComboBoxRenderer;
import org.apache.log4j.Logger;
import org.obo.annotation.view.TermRenderer;
import org.obo.app.swing.BugWorkaroundTable;
import org.obo.app.swing.PlaceholderRenderer;
import org.obo.app.swing.SortDisabler;
import org.obo.app.util.EverythingEqualComparator;
import org.phenoscape.controller.PhenexController;
import org.phenoscape.model.Association;
import org.phenoscape.model.Character;
import org.phenoscape.model.DataSet;
import org.phenoscape.model.MatrixCell;
import org.phenoscape.model.MultipleState;
import org.phenoscape.model.MultipleState.MODE;
import org.phenoscape.model.State;
import org.phenoscape.model.Taxon;
import ca.odell.glazedlists.CollectionList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.gui.AdvancedTableFormat;
import ca.odell.glazedlists.gui.WritableTableFormat;
import ca.odell.glazedlists.swing.EventTableModel;
import ca.odell.glazedlists.swing.TableComparatorChooser;
import com.eekboom.utils.Strings;
public class CharacterMatrixComponent extends PhenoscapeGUIComponent {
private EventTableModel<Taxon> headerModel;
private EventTableModel<Taxon> matrixTableModel;
private JTable matrixTable;
private DefaultCellEditor popupEditor;
private DefaultCellEditor quickEditor;
private final SortedList<Taxon> sortedTaxa = new SortedList<Taxon>(this.getController().getDataSet().getTaxa(), new EverythingEqualComparator<Taxon>());
private final EventList<State> allStates;
private static enum TaxonDisplay {
PUBLICATION_NAME { @Override public String toString() { return "Display Publication Name"; }},
VALID_NAME { @Override public String toString() { return "Display Valid Name"; }},
MATRIX_NAME { @Override public String toString() { return "Display Matrix Name"; }}
}
private static enum CharacterDisplay {
CHARACTER_NUMBER { @Override public String toString() { return "Display Character Number"; }},
CHARACTER_DESCRIPTION { @Override public String toString() { return "Display Character Description"; }}
}
private static enum StateDisplay {
STATE_SYMBOL { @Override public String toString() { return "Display State Symbol"; }},
STATE_DESCRIPTION { @Override public String toString() { return "Display State Description"; }}
}
private TaxonDisplay taxonOption = TaxonDisplay.VALID_NAME;
private CharacterDisplay characterOption = CharacterDisplay.CHARACTER_NUMBER;
private StateDisplay stateOption = StateDisplay.STATE_SYMBOL;
public CharacterMatrixComponent(String id, PhenexController controller) {
super(id, controller);
this.allStates = new CollectionList<Character, State>(this.getController().getDataSet().getCharacters(),
new CollectionList.Model<Character, State>() {
@Override
public List<State> getChildren(Character parent) {
return parent.getStates();
}
}
);
}
@Override
public void init() {
super.init();
this.initializeInterface();
}
private void initializeInterface() {
this.setLayout(new BorderLayout());
this.headerModel = new EventTableModel<Taxon>(this.sortedTaxa, new HeaderTableFormat());
final JTable headerTable = new BugWorkaroundTable(this.headerModel);
headerTable.putClientProperty("Quaqua.Table.style", "striped");
headerTable.setDefaultRenderer(Taxon.class, new TaxonRenderer());
headerTable.getColumnModel().getColumn(0).setMaxWidth(40);
final TableComparatorChooser<Taxon> sortChooser = new TableComparatorChooser<Taxon>(headerTable, this.sortedTaxa, false);
sortChooser.addSortActionListener(new SortDisabler());
this.matrixTableModel = new EventTableModel<Taxon>(this.sortedTaxa, new MatrixTableFormat());
this.matrixTable = new BugWorkaroundTable(this.matrixTableModel);
this.matrixTable.setCellSelectionEnabled(true);
this.matrixTable.setDefaultRenderer(Object.class, new PlaceholderRenderer("None"));
this.matrixTable.setDefaultRenderer(State.class, new StateCellRenderer());
final JComboBox statesBox = new JComboBox();
statesBox.setRenderer(new StateListRenderer());
this.popupEditor = new PopupStateCellEditor(statesBox);
this.popupEditor.setClickCountToStart(2);
this.quickEditor = new QuickStateCellEditor(new JTextField());
this.matrixTable.setDefaultEditor(State.class, this.popupEditor);
this.matrixTable.putClientProperty("Quaqua.Table.style", "striped");
this.matrixTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
this.getController().getDataSet().getCharacters().addListEventListener(new ListEventListener<Character>() {
@Override
public void listChanged(ListEvent<Character> listChanges) {
matrixTableModel.fireTableStructureChanged();
}
});
this.allStates.addListEventListener(new ListEventListener<State>() {
@Override
public void listChanged(ListEvent<State> listChanges) {
matrixTableModel.fireTableDataChanged();
}
});
final JScrollPane headerScroller = new JScrollPane(headerTable);
final JScrollPane matrixScroller = new JScrollPane(matrixTable);
headerScroller.getVerticalScrollBar().setModel(matrixScroller.getVerticalScrollBar().getModel());
headerScroller.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
headerScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
matrixScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, headerScroller, matrixScroller);
splitPane.setDividerLocation(150);
splitPane.setDividerSize(3);
this.add(splitPane, BorderLayout.CENTER);
this.add(this.createToolBar(), BorderLayout.SOUTH);
this.getController().getDataSet().addPropertyChangeListener(DataSet.MATRIX_CELL, new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
matrixTableModel.fireTableDataChanged();
}
});
this.matrixTable.getColumnModel().getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
updateSelectedCell();
}
});
this.matrixTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
updateSelectedCell();
}
});
}
private void updateSelectedCell() {
final Taxon selectedTaxon;
if (this.matrixTable.getSelectedRow() != -1) {
selectedTaxon = this.matrixTableModel.getElementAt(this.matrixTable.getSelectedRow());
} else {
selectedTaxon = null;
}
final Character selectedCharacter;
if (this.matrixTable.getSelectedColumn() != -1) {
selectedCharacter = this.getCharacter(this.matrixTable.getSelectedColumn());
} else {
selectedCharacter = null;
}
if (selectedTaxon != null && selectedCharacter != null) {
this.getController().setSelectedMatrixCell(new MatrixCell(selectedTaxon, selectedCharacter));
} else {
this.getController().setSelectedMatrixCell(null);
}
}
private JToolBar createToolBar() {
final JToolBar toolBar = new JToolBar();
final JComboBox taxonBox = new JComboBox(TaxonDisplay.values());
taxonBox.setSelectedItem(this.taxonOption);
taxonBox.setAction(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
taxonOption = (TaxonDisplay)(taxonBox.getSelectedItem());
headerModel.fireTableDataChanged();
}
});
final JComboBox characterBox = new JComboBox(CharacterDisplay.values());
characterBox.setSelectedItem(this.characterOption);
characterBox.setAction(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
characterOption = (CharacterDisplay)(characterBox.getSelectedItem());
matrixTableModel.fireTableStructureChanged();
}
});
final JComboBox stateBox = new JComboBox(StateDisplay.values());
stateBox.setSelectedItem(this.stateOption);
stateBox.setAction(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
stateOption = (StateDisplay)(stateBox.getSelectedItem());
matrixTableModel.fireTableDataChanged();
}
});
final JCheckBox editorTypeCheckBox = new JCheckBox("Use quick editor");
editorTypeCheckBox.setSelected(false);
editorTypeCheckBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
matrixTable.setDefaultEditor(State.class, quickEditor);
} else {
matrixTable.setDefaultEditor(State.class, popupEditor);
}
}});
toolBar.add(taxonBox);
toolBar.add(characterBox);
toolBar.add(stateBox);
toolBar.add(editorTypeCheckBox);
toolBar.setFloatable(false);
return toolBar;
}
private Character getCharacter(int index) {
return getController().getDataSet().getCharacters().get(index);
}
private class HeaderTableFormat implements AdvancedTableFormat<Taxon> {
@Override
public Class<?> getColumnClass(int column) {
switch (column) {
case 0: return Integer.class;
case 1: return Taxon.class;
default: return null;
}
}
@Override
public Comparator<?> getColumnComparator(int column) {
switch (column) {
case 0: return GlazedLists.comparableComparator();
case 1: return new Comparator<Taxon>() {
@Override
public int compare(Taxon o1, Taxon o2) {
if (taxonOption.equals(TaxonDisplay.VALID_NAME)) {
return GlazedLists.comparableComparator().compare(o1.getValidName(), o2.getValidName());
} else if (taxonOption.equals(TaxonDisplay.PUBLICATION_NAME)){
return Strings.getNaturalComparator().compare(o1.getPublicationName(), o2.getPublicationName());
} else { // MATRIX_NAME
return Strings.getNaturalComparator().compare(o1.getMatrixTaxonName(), o2.getMatrixTaxonName());
}
}
};
default: return null;
}
}
@Override
public int getColumnCount() {
return 2;
}
@Override
public String getColumnName(int column) {
switch(column) {
case 0: return " ";
case 1: return "Taxon";
default: return null;
}
}
@Override
public Object getColumnValue(Taxon taxon, int column) {
switch(column) {
case 0: return getController().getDataSet().getTaxa().indexOf(taxon) + 1;
case 1: return taxon;
default: return null;
}
}
}
private class MatrixTableFormat implements AdvancedTableFormat<Taxon>, WritableTableFormat<Taxon> {
@Override
public int getColumnCount() {
return getController().getDataSet().getCharacters().size();
}
@Override
public Class<?> getColumnClass(int column) {
return State.class;
}
@Override
public Comparator<?> getColumnComparator(int column) {
return null;
}
@Override
public String getColumnName(int column) {
if (characterOption.equals(CharacterDisplay.CHARACTER_DESCRIPTION)) {
return getCharacter(column).toString();
} else if (characterOption.equals(CharacterDisplay.CHARACTER_NUMBER)) {
return "" + (column + 1);
}
return null;
}
@Override
public Object getColumnValue(Taxon taxon, int column) {
return getController().getDataSet().getStateForTaxon(taxon, getCharacter(column));
}
@Override
public boolean isEditable(Taxon baseObject, int column) {
return true;
}
@Override
public Taxon setColumnValue(Taxon taxon, Object editedValue, int column) {
getController().getDataSet().setStateForTaxon(taxon, getCharacter(column), (State)editedValue);
return taxon;
}
}
private class StateCellRenderer extends PlaceholderRenderer {
public StateCellRenderer() {
super("?");
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
final State state = (State)value;
if (state != null) {
final Object newValue;
if (stateOption.equals(StateDisplay.STATE_SYMBOL)) {
newValue = state.getSymbol() != null ? state.getSymbol() : "#";
} else {
newValue = state.getLabel() != null ? state.getLabel() : "untitled";
}
return super.getTableCellRendererComponent(table, newValue, isSelected, hasFocus, row, column);
} else {
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
}
}
}
private class StateListRenderer extends BasicComboBoxRenderer {
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
final Object newValue = value != null ? value : "?";
return super.getListCellRendererComponent(list, newValue, index, isSelected, cellHasFocus);
}
}
private class PopupStateCellEditor extends DefaultCellEditor {
private final DefaultComboBoxModel model = new DefaultComboBoxModel();
public PopupStateCellEditor(JComboBox comboBox) {
super(comboBox);
comboBox.setModel(model);
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
final Character character = getController().getDataSet().getCharacters().get(column);
this.model.removeAllElements();
this.model.addElement(null);
for (State state : character.getStates()) {
this.model.addElement(state);
}
return super.getTableCellEditorComponent(table, value, isSelected, row, column);
}
}
private class QuickStateCellEditor extends DefaultCellEditor {
private List<State> states = new ArrayList<State>();
private State originalValue;
private State currentValue;
private final JTextField field;
private boolean invalid = false;
public QuickStateCellEditor(JTextField textField) {
super(textField);
this.field = textField;
this.field.setBorder(BorderFactory.createLineBorder(Color.BLACK));
textField.getDocument().addDocumentListener(new FieldListener());
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
final Character character = getController().getDataSet().getCharacters().get(column);
this.states = character.getStates();
final State state = (State)value;
final Object newValue = state != null ? state.getSymbol() : null;
this.originalValue = state;
this.currentValue = state;
final Component component = super.getTableCellEditorComponent(table, newValue, isSelected, row, column);
this.field.selectAll();
return component;
}
@Override
public void cancelCellEditing() {
this.currentValue = this.originalValue;
super.cancelCellEditing();
}
@Override
public Object getCellEditorValue() {
return this.currentValue;
}
@Override
public boolean stopCellEditing() {
if (this.getInvalid()) {
return false;
}
return super.stopCellEditing();
}
private boolean getInvalid() {
return this.invalid;
}
private void setInvalid(boolean value) {
this.invalid = value;
if (this.invalid) {
field.setForeground(Color.RED);
} else {
field.setForeground(Color.BLACK);
}
}
private class FieldListener implements DocumentListener {
@Override
public void changedUpdate(DocumentEvent e) { this.documentChanged(); }
@Override
public void insertUpdate(DocumentEvent e) { this.documentChanged(); }
@Override
public void removeUpdate(DocumentEvent e) { this.documentChanged(); }
private void documentChanged() {
final String text = field.getText();
if ((text == null) || (text.equals(""))) {
currentValue = null;
setInvalid(false);
} else {
final State foundState = this.interpretEntry(text);
if (foundState != null) {
currentValue = foundState;
setInvalid(false);
} else {
setInvalid(true);
}
}
}
private State interpretEntry(String text) {
log().debug("Interpret entry: " + text);
if (text.contains("&")) {
final Set<State> multipleStates = this.interpretStateSymbols(text.split("&"));
if (multipleStates != null) {
return new MultipleState(multipleStates, MODE.POLYMORPHIC);
} else {
return null;
}
} else if (text.contains("/")) {
final Set<State> multipleStates = this.interpretStateSymbols(text.split("/"));
if (multipleStates != null) {
return new MultipleState(multipleStates, MODE.UNCERTAIN);
} else {
return null;
}
} else {
for (State state : states) {
if (text.equals(state.getSymbol())) {
return state;
}
}
}
return null;
}
private Set<State> interpretStateSymbols(String[] symbols) {
if (symbols.length == 1) {
return null;
}
final Set<State> multipleStates = new HashSet<State>();
for (String symbol : symbols) {
for (State state : states) {
if (symbol.equals(state.getSymbol())) {
multipleStates.add(state);
}
}
}
if (symbols.length == multipleStates.size()) {
return multipleStates;
} else {
return null;
}
}
}
}
private class TaxonRenderer extends TermRenderer {
public TaxonRenderer() {
super("untitled");
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
if (taxonOption.equals(TaxonDisplay.VALID_NAME)) {
final Object newValue = value != null ? ((Taxon)value).getValidName() : value;
return super.getTableCellRendererComponent(table, newValue, isSelected, hasFocus, row, column);
} else {
final Object newValue;
if (value != null) {
newValue = taxonOption.equals(TaxonDisplay.PUBLICATION_NAME) ? ((Taxon)value).getPublicationName() : ((Taxon)value).getMatrixTaxonName();
} else {
newValue = value;
}
return super.getTableCellRendererComponent(table, newValue, isSelected, hasFocus, row, column);
}
}
}
@Override
protected Logger log() {
return Logger.getLogger(this.getClass());
}
}