// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.datatype; import java.awt.Color; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionListener; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.table.AbstractTableModel; import org.infinity.gui.BrowserMenuBar; import org.infinity.gui.StructViewer; import org.infinity.gui.ViewerUtil; import org.infinity.icon.Icons; import org.infinity.resource.AbstractStruct; import org.infinity.resource.StructEntry; /** * A Number object consisting of multiple values of a given number of bits. */ public class MultiNumber extends Datatype implements Editable, IsNumeric { private int value; private ValueTableModel mValues; private JTable tValues; /** * Constructs a Number object consisting of multiple values of a given number of bits. * @param buffer The buffer containing resource data for this type. * @param offset Resource offset * @param length Resource length in bytes. Supported lengths: 1, 2, 3, 4 * @param name Field name * @param numBits Number of bits for each value being part of the Number object. * @param numValues Number of values to consider. Supported range: [1, length*8/numBits] * @param valueNames List of individual field names for each contained value. */ public MultiNumber(ByteBuffer buffer, int offset, int length, String name, int numBits, int numValues, String[] valueNames) { this(null, buffer, offset, length, name, numBits, numValues, valueNames); } /** * Constructs a Number object consisting of multiple values of a given number of bits. * @param parent A parent structure containing to this datatype object. * @param buffer The buffer containing resource data for this type. * @param offset Resource offset * @param length Resource length in bytes. Supported lengths: 1, 2, 3, 4 * @param name Field name * @param numBits Number of bits for each value being part of the Number object. * @param numValues Number of values to consider. Supported range: [1, length*8/numBits] * @param valueNames List of individual field names for each contained value. */ public MultiNumber(StructEntry parent, ByteBuffer buffer, int offset, int length, String name, int numBits, int numValues, String[] valueNames) { super(offset, length, name); read(buffer, offset); if (numBits < 1 || numBits > (length*8)) numBits = length*8; if (numValues < 1) { numValues = 1; } else if (numValues > (length*8/numBits)) { numValues = length*8/numBits; } mValues = new ValueTableModel(value, numBits, numValues, valueNames); } //--------------------- Begin Interface Editable --------------------- @Override public JComponent edit(ActionListener container) { tValues = new JTable(mValues); tValues.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); tValues.setFont(BrowserMenuBar.getInstance().getScriptFont()); tValues.setBorder(BorderFactory.createLineBorder(Color.GRAY)); tValues.getTableHeader().setBorder(BorderFactory.createLineBorder(Color.GRAY)); tValues.getTableHeader().setReorderingAllowed(false); tValues.getTableHeader().setResizingAllowed(true); tValues.setPreferredScrollableViewportSize(tValues.getPreferredSize()); JScrollPane scroll = new JScrollPane(tValues); scroll.setBorder(BorderFactory.createEmptyBorder()); JButton bUpdate = new JButton("Update value", Icons.getIcon(Icons.ICON_REFRESH_16)); bUpdate.addActionListener(container); bUpdate.setActionCommand(StructViewer.UPDATE_VALUE); JPanel panel = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0); panel.add(scroll, gbc); gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 8, 0, 0), 0, 0); panel.add(bUpdate, gbc); panel.setPreferredSize(DIM_MEDIUM); // making "Attribute" column wider int tableWidth = DIM_MEDIUM.width - bUpdate.getPreferredSize().width - 8; tValues.getColumnModel().getColumn(0).setPreferredWidth(tableWidth * 3 / 4); tValues.getColumnModel().getColumn(1).setPreferredWidth(tableWidth * 1 / 4); return panel; } @Override public void select() { mValues.setValue(value); } @Override public boolean updateValue(AbstractStruct struct) { value = mValues.getValue(); // notifying listeners fireValueUpdated(new UpdateEvent(this, struct)); return true; } //--------------------- End Interface Editable --------------------- //--------------------- Begin Interface Writeable --------------------- @Override public void write(OutputStream os) throws IOException { writeInt(os, value); } //--------------------- End Interface Writeable --------------------- //--------------------- Begin Interface Readable --------------------- @Override public int read(ByteBuffer buffer, int offset) { buffer.position(offset); switch (getSize()) { case 1: value = buffer.get(); break; case 2: value = buffer.getShort(); break; case 3: value = buffer.getInt() & 0xffffff; value <<= 8; value >>= 8; // sign-extend break; case 4: value = buffer.getInt(); break; default: throw new IllegalArgumentException(); } return offset + getSize(); } //--------------------- End Interface Readable --------------------- @Override public String toString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < getValueCount(); i++) { sb.append(String.format("%1$s: %2$d", getValueName(i), getValue(i))); if (i+1 < getValueCount()) sb.append(", "); } return sb.toString(); } /** Returns number of bits per value. */ public int getBits() { return mValues.getBitsPerValue(); } /** Returns number of values stored in the Number object. */ public int getValueCount() { return mValues.getValueCount(); } /** Returns the label associated with the specified value. */ public String getValueName(int idx) { return mValues.getValueName(idx); } //--------------------- Begin Interface IsNumeric --------------------- @Override public long getLongValue() { return (long)value & 0xffffffffL; } @Override public int getValue() { return value; } //--------------------- End Interface IsNumeric --------------------- /** Returns the specified value. */ public int getValue(int idx) { if (idx >= 0 && idx < mValues.getValueCount()) { if (getBits() < 32) { return (value >>> (idx*getBits())) & ((1 << getBits()) - 1); } else { return getValue(); } } return 0; } /** Set the value for the whole Number object. */ public void setValue(int value) { mValues.setValue(value); this.value = mValues.getValue(); } /** Sets the specified value. */ public void setValue(int idx, int value) { mValues.setValue(idx, value); this.value = mValues.getValue(); } //-------------------------- INNER CLASSES -------------------------- // Manages a fixed two columns table with a given number of rows private static class ValueTableModel extends AbstractTableModel { private static final int ATTRIBUTE = 0; private static final int VALUE = 1; private final Object[][] data; private int bits; private int numValues; public ValueTableModel(Integer value, int bits, int numValues, String[] labels) { if (bits < 1) bits = 1; else if (bits > 32) bits = 32; if (numValues < 1 || numValues > (32 / bits)) numValues = 32 / bits; this.bits = bits; this.numValues = numValues; data = new Object[2][numValues]; for (int i = 0; i < numValues; i++) { if (labels != null && i < labels.length && labels[i] != null) { data[ATTRIBUTE][i] = labels[i]; } else { data[ATTRIBUTE][i] = "Value " + Integer.toString(i+1); } data[VALUE][i] = Integer.valueOf((value >>> (i*bits)) & ((1 << bits) - 1)); } } //--------------------- Begin Class AbstractTableModel --------------------- @Override public int getRowCount() { return numValues; } @Override public int getColumnCount() { return 2; // fixed } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (rowIndex >= 0 && rowIndex < numValues && columnIndex >= 0 && columnIndex < 2) { return data[columnIndex][rowIndex]; } return null; } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { if (columnIndex == 1) { if (rowIndex >= 0 && rowIndex < numValues) { try { int newVal = Integer.parseInt(aValue.toString()); if (newVal < 0) newVal = 0; if (newVal >= (1 << bits)) newVal = (1 << bits) - 1; data[VALUE][rowIndex] = Integer.valueOf(newVal); fireTableCellUpdated(rowIndex, columnIndex); } catch (NumberFormatException e) { e.printStackTrace(); } } } } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return (columnIndex == VALUE); } @Override public Class<?> getColumnClass(int columnIndex) { if (columnIndex >= 0 && columnIndex < 2) { return getValueAt(0, columnIndex).getClass(); } else { return Object.class; } } @Override public String getColumnName(int columnIndex) { switch (columnIndex) { case ATTRIBUTE: return "Attribute"; case VALUE: return "Value"; default: return ""; } } //--------------------- End Class AbstractTableModel --------------------- @Override public String toString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < numValues; i++) { sb.append(String.format("%1$s: %2$d", (String)data[ATTRIBUTE][i], ((Integer)data[VALUE][i]).intValue())); if (i+1 < numValues) sb.append(", "); } return sb.toString(); } public int getValue() { int retVal = 0; for (int i = 0; i < numValues; i++) { retVal |= (getValue(i) & ((1 << bits) - 1)) << (i*bits); } return retVal; } public int getValue(int rowIndex) { if (rowIndex >= 0 && rowIndex < numValues) { return ((Integer)data[VALUE][rowIndex]).intValue(); } return 0; } public void setValue(int v) { for (int i = 0; i < numValues; i++, v >>>= bits) { data[VALUE][i] = Integer.valueOf(v & ((1 << bits) - 1)); } } public void setValue(int rowIndex, int v) { if (rowIndex >= 0 && rowIndex < numValues) { data[VALUE][rowIndex] = Integer.valueOf(v & ((1 << bits) - 1)); } } public int getValueCount() { return numValues; } public int getBitsPerValue() { return bits; } public String getValueName(int rowIndex) { if (rowIndex >= 0 && rowIndex < numValues) { return (String)data[ATTRIBUTE][rowIndex]; } return ""; } } }