/*
* StatTableModel.java
* Copyright 2010 (C) Connor Petty <cpmeister@users.sourceforge.net>
*
* This library 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.
*
* This library 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 this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Created on May 11, 2010, 2:01:06 PM
*/
package pcgen.gui2.tabs.summary;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.text.DecimalFormat;
import java.text.ParseException;
import javax.swing.AbstractAction;
import javax.swing.AbstractCellEditor;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import pcgen.facade.core.CharacterFacade;
import pcgen.facade.core.StatFacade;
import pcgen.facade.util.event.ReferenceEvent;
import pcgen.facade.util.event.ReferenceListener;
import pcgen.facade.util.ListFacade;
import pcgen.gui2.tabs.Utilities;
import pcgen.gui2.util.FontManipulation;
import pcgen.gui2.util.PrettyIntegerFormat;
import pcgen.gui2.util.table.TableCellUtilities;
/**
* Model used for the Ability/statistics table.
*
* @author Connor Petty <cpmeister@users.sourceforge.net>
*/
public class StatTableModel extends AbstractTableModel implements ReferenceListener<Number>
{
public static final String EDITABLE_COLUMN_ID = "EDITABLE"; //$NON-NLS-1$
private static final String ABILITY_COLUMN_ID = "ABILITY"; //$NON-NLS-1$
private static final String MOVEDOWN = "movedown"; //$NON-NLS-1$
private static final int ABILITY_NAME = 0;
private static final int EDITABLE_SCORE = 3;
private static final int RACE_ADJ = 4;
private static final int MISC_ADJ = 5;
private static final int FINAL_ABILITY_SCORE = 1;
private static final int ABILITY_MOD = 2;
private final CharacterFacade character;
private final ListFacade<StatFacade> stats;
private final StatRenderer renderer = new StatRenderer();
private final SpinnerEditor editor = new SpinnerEditor();
private final JTable table;
public StatTableModel(CharacterFacade character, JTable jtable)
{
this.character = character;
this.table = jtable;
this.stats = character.getDataSet().getStats();
int min = Integer.MAX_VALUE;
for (StatFacade sf : stats)
{
min = Math.min(sf.getMinValue(), min);
}
editor.setMinValue(min);
final JTextField field = editor.getTextField();
InputMap map = field.getInputMap(JComponent.WHEN_FOCUSED);
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), MOVEDOWN);
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), MOVEDOWN);
Action action = new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent e)
{
//Logging.log(Logging.WARNING, "Got handleEnter from " + e.getSource());
int row = table.getEditingRow();
final int col = table.getEditingColumn();
table.getCellEditor().stopCellEditing(); // store user input
final int nextRow = row + 1;
startEditingNextRow(table, col, nextRow, field);
}
};
field.getActionMap().put(MOVEDOWN, action);
}
private void startEditingNextRow(final JTable statsTable,
final int col, final int nextRow, JTextField textField)
{
if (nextRow >= 0 && nextRow < getRowCount()
&& col >= 0 && col < getColumnCount())
{
statsTable.editCellAt(nextRow, col);
textField.requestFocusInWindow();
}
}
@Override
public int getRowCount()
{
return stats.getSize();
}
public static void initializeTable(JTable statsTable)
{
JTableHeader tableHeader = statsTable.getTableHeader();
tableHeader.setResizingAllowed(false);
tableHeader.setReorderingAllowed(false);
statsTable.setAutoCreateColumnsFromModel(false);
DefaultTableColumnModel columnModel = new DefaultTableColumnModel();
{
TableColumn column = Utilities.createTableColumn(ABILITY_NAME, "Ability", new AbilityHeaderCellRenderer(), true);
column.setIdentifier(ABILITY_COLUMN_ID);
columnModel.addColumn(column);
String htmlText = "<html><div align=\"center\">Final<br>Score</div></html>";
column = Utilities.createTableColumn(FINAL_ABILITY_SCORE, htmlText, new FixedHeaderCellRenderer(htmlText), false);
column.setCellRenderer(new ValueRenderer());
columnModel.addColumn(column);
TableCellRenderer renderer = new ModRenderer();
htmlText = "<html><div align=\"center\">Ability<br>Mod</div></html>";
column = Utilities.createTableColumn(ABILITY_MOD, htmlText, new FixedHeaderCellRenderer(htmlText), false);
column.setCellRenderer(renderer);
columnModel.addColumn(column);
htmlText = "<html><div align=\"center\">Editable<br>Score</div></html>";
column = Utilities.createTableColumn(EDITABLE_SCORE, htmlText, new FixedHeaderCellRenderer(htmlText), false);
column.setIdentifier(EDITABLE_COLUMN_ID);
columnModel.addColumn(column);
htmlText = "<html><div align=\"center\">Race<br>Adj</div></html>";
column = Utilities.createTableColumn(RACE_ADJ, htmlText, new FixedHeaderCellRenderer(htmlText), false);
column.setCellRenderer(renderer);
columnModel.addColumn(column);
htmlText = "<html><div align=\"center\">Misc<br>Adj</div></html>";
column = Utilities.createTableColumn(MISC_ADJ, htmlText, new FixedHeaderCellRenderer(htmlText), false);
column.setCellRenderer(renderer);
columnModel.addColumn(column);
}
statsTable.setColumnModel(columnModel);
statsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
statsTable.setShowVerticalLines(false);
statsTable.setCellSelectionEnabled(false);
statsTable.setFocusable(false);
// XXX this should be calculated relative to font size and the size of a jspinner
statsTable.setRowHeight(27);
statsTable.setOpaque(false);
tableHeader.setFont(FontManipulation.title(statsTable.getFont()));
FontManipulation.large(statsTable);
}
/**
* This renders the header for the Ability name. Mostly this just
* delegates to the L&F default table header renderer but centers the
* resulting label.
*/
private static class AbilityHeaderCellRenderer implements TableCellRenderer
{
/* (non-Javadoc)
* @see javax.swing.table.TableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
*/
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
Component comp = table.getTableHeader().getDefaultRenderer().getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
JLabel label = (JLabel) comp;
label.setHorizontalAlignment(SwingConstants.CENTER);
return label;
}
}
/*
* This class is a hack that gives the TableHeaderUI a dummy component
* so that it can be used when calculating the height of the JTableHeader.
*/
private static class FixedHeaderCellRenderer extends JLabel implements TableCellRenderer
{
public FixedHeaderCellRenderer(String text)
{
setText(text);
setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 10));
}
/* (non-Javadoc)
* @see javax.swing.table.TableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
*/
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
if (table == null)
{
return this;
}
return table.getTableHeader().getDefaultRenderer().getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
}
}
private static class ModRenderer extends JLabel implements TableCellRenderer
{
private DecimalFormat formatter = PrettyIntegerFormat.getFormat();
public ModRenderer()
{
setHorizontalAlignment(SwingConstants.RIGHT);
setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 7));
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
Font tableFont = table.getFont();
if (column < 3)
{
setFont(FontManipulation.bold(tableFont));
}
else
{
setFont(FontManipulation.plain(tableFont));
}
setBackground(table.getBackground());
setForeground(table.getForeground());
Integer mod = (Integer) value;
if (mod.intValue() == 0 && column > 3)
{
// let's use a pretty em dash instead of hyphen/minus.
setText("\u2014");
}
else
{
setText(formatter.format(mod.longValue()));
}
return this;
}
}
/**
* The Class {@code ValueRenderer} displays a right aligned
* read-only column containing a string value.
*/
private static class ValueRenderer extends JLabel implements TableCellRenderer
{
/**
* Create a new ValueRenderer instance.
*/
public ValueRenderer()
{
setHorizontalAlignment(SwingConstants.RIGHT);
setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 7));
}
/* (non-Javadoc)
* @see javax.swing.table.TableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
*/
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
setFont(FontManipulation.title(table.getFont()));
setBackground(table.getBackground());
setForeground(table.getForeground());
setText((String) value);
return this;
}
}
public void install()
{
table.setModel(this);
table.setDefaultRenderer(Object.class, renderer);
TableColumn abilityColumn = table.getColumn(ABILITY_COLUMN_ID);
int columnIndex = abilityColumn.getModelIndex();
int maxWidth = 0;
for (StatFacade aStat : stats)
{
Component cell = renderer.getTableCellRendererComponent(table,
aStat,
false, false, -1, columnIndex);
maxWidth = Math.max(maxWidth, cell.getPreferredSize().width);
}
//we add some extra spacing to prevent ellipses from showing
abilityColumn.setPreferredWidth(maxWidth + 4);
TableColumn column = table.getColumn(EDITABLE_COLUMN_ID);
column.setCellRenderer(new TableCellUtilities.SpinnerRenderer());
column.setCellEditor(editor);
Dimension size = table.getPreferredSize();
size.width = table.getTableHeader().getPreferredSize().width;
JScrollPane scrollPane = (JScrollPane) table.getParent().getParent();
//we want to add room for the vertical scroll bar so it doesn't
//overlap with the table when it shows
int vbarWidth = scrollPane.getVerticalScrollBar().getPreferredSize().width;
size.width += vbarWidth;
table.setPreferredScrollableViewportSize(size);
//because of the extra viewport size in the table it will
//always look a bit off center, adding a row header to
//the scroll pane fixes this
scrollPane.setRowHeaderView(Box.createHorizontalStrut(vbarWidth));
for (StatFacade aStat : stats)
{
character.getScoreBaseRef(aStat).addReferenceListener(this);
}
}
public void uninstall()
{
if (table.isEditing() && !editor.stopCellEditing())
{
editor.cancelCellEditing();
}
for (StatFacade aStat : stats)
{
character.getScoreBaseRef(aStat).removeReferenceListener(this);
}
}
public static class SpinnerEditor extends AbstractCellEditor
implements TableCellEditor, ChangeListener
{
protected final JSpinner spinner;
public SpinnerEditor()
{
this.spinner = new JSpinner(new SpinnerNumberModel(0, 0, null, 1));
}
/**
* Set a new lower bound for the spinner.
*
* @param minValue The new minimum value.
*/
public void setMinValue(int minValue)
{
SpinnerNumberModel spinnerModel = (SpinnerNumberModel) spinner.getModel();
spinnerModel.setMinimum(minValue);
}
public JTextField getTextField()
{
return ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField();
}
@Override
public Object getCellEditorValue()
{
return spinner.getValue();
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value,
boolean isSelected,
int row,
int column)
{
spinner.setValue(value);
spinner.addChangeListener(this);
return spinner;
}
@Override
public void stateChanged(ChangeEvent e)
{
stopCellEditing();
}
@Override
public boolean stopCellEditing()
{
try
{
spinner.removeChangeListener(this);
spinner.commitEdit();
}
catch (ParseException ex)
{
return false;
}
return super.stopCellEditing();
}
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex)
{
return columnIndex == EDITABLE_SCORE;
}
@Override
public Class<?> getColumnClass(int columnIndex)
{
if (columnIndex == ABILITY_NAME)
{
return Object.class;
}
return Integer.class;
}
@Override
public int getColumnCount()
{
return 6;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex)
{
StatFacade stat = stats.getElementAt(rowIndex);
switch (columnIndex)
{
case ABILITY_NAME:
return stat;
case ABILITY_MOD:
return character.getModTotal(stat);
case EDITABLE_SCORE:
return character.getScoreBase(stat);
case RACE_ADJ:
return character.getScoreRaceBonus(stat);
case FINAL_ABILITY_SCORE:
return character.getScoreTotalString(stat);
case MISC_ADJ:
return character.getScoreOtherBonus(stat);
default:
return 0;
}
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex)
{
Number number = (Number) aValue;
character.setScoreBase(stats.getElementAt(rowIndex), number.intValue());
fireTableRowsUpdated(rowIndex, rowIndex);
}
@Override
public void referenceChanged(ReferenceEvent<Number> e)
{
fireTableDataChanged();
}
/**
* Table renderer used for abilities/statistics.
*/
private class StatRenderer extends JLabel implements TableCellRenderer
{
@Override
public Component getTableCellRendererComponent(JTable jTable,
Object value, boolean isSelected, boolean hasFocus, int row,
int column)
{
setFont(FontManipulation.title(jTable.getFont()));
// Those two does not seem to change anything.
setBackground(jTable.getBackground());
setForeground(jTable.getForeground());
StatFacade stat = (StatFacade) value;
//TODO: this should really call stat.toString()
setText(stat.getName());
return this;
}
}
}