/* * Copyright (c) 2005 (Mike) Maurice Kienenberger (mkienenb@gmail.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package org.gamenet.swing.controls; import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.TransferHandler; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableColumnModel; import org.gamenet.util.ByteConversions; public class ByteDataTableControl extends JPanel { private final ByteDataTableModel byteDataTableModel; public ByteDataTableControl(byte[] byteData, int byteDataColumns, long baseOffsetForDisplay) { this(byteData, byteDataColumns, baseOffsetForDisplay, 0, byteData.length); } public ByteDataTableControl(byte[] byteData, int byteDataColumns, long baseOffsetForDisplay, int inclusiveStartOffset, int exclusiveEndOffset) { super(new BorderLayout()); this.byteDataTableModel = new ByteDataTableModel(byteDataColumns, baseOffsetForDisplay, byteData, inclusiveStartOffset, exclusiveEndOffset); if ((exclusiveEndOffset - inclusiveStartOffset) == 0) { this.add(new JLabel("No data."), BorderLayout.CENTER); return; } final ResizingJTable dataTable = new ResizingJTable(byteDataTableModel) { public String getToolTipText(MouseEvent e) { String tip = null; java.awt.Point p = e.getPoint(); int rowIndex = rowAtPoint(p); int colIndex = columnAtPoint(p); int realColumnIndex = convertColumnIndexToModel(colIndex); if (0 == realColumnIndex) return null; return byteDataTableModel.getOffsetDisplayString(rowIndex, realColumnIndex); } }; JComboBox displayModeComboBox = new JComboBox(byteDataTableModel.displayModeNameArray); displayModeComboBox.setSelectedIndex(this.byteDataTableModel.getDisplayMode()); displayModeComboBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent anActionEvent) { JComboBox cb = (JComboBox)anActionEvent.getSource(); byteDataTableModel.setDisplayMode(cb.getSelectedIndex()); dataTable.recalcColumnWidths(); } } ); JLabel columnCountTextFieldLabel = new JLabel("Bytes per line: "); final JTextField columnCountTextField = new JTextField(4); columnCountTextField.setText(String.valueOf(byteDataColumns)); JButton columnCountTextFieldButton = new JButton("Change bytes per line"); columnCountTextFieldButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent anActionEvent) { try { int newColumnCount = Integer.parseInt(columnCountTextField.getText()); byteDataTableModel.setColumnCount(newColumnCount + 1); dataTable.recalcColumnWidths(); } catch (NumberFormatException e) { e.printStackTrace(); } } } ); JLabel paddingTextFieldLabel = new JLabel("bytes of padding: "); final JTextField paddingTextField = new JTextField(4); paddingTextField.setText(String.valueOf(0)); JButton paddingTextFieldButton = new JButton("Change initial padding bytes"); paddingTextFieldButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent anActionEvent) { try { int newPadding = Integer.parseInt(paddingTextField.getText()); byteDataTableModel.setPadding(newPadding); } catch (NumberFormatException e) { e.printStackTrace(); } } } ); final JButton copyToClipboardTextFieldButton = new JButton("Copy to clipboard"); TransferHandler handler = new TransferHandler("text") { protected Transferable createTransferable(JComponent c) { return new StringSelection(byteDataTableModel.exportDataAsCSV()); } }; copyToClipboardTextFieldButton.setTransferHandler(handler); copyToClipboardTextFieldButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent anActionEvent) { Action copyAction = TransferHandler.getCopyAction(); copyAction.actionPerformed(new ActionEvent(copyToClipboardTextFieldButton, ActionEvent.ACTION_PERFORMED, null)); } } ); JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); controlPanel.add(displayModeComboBox); controlPanel.add(copyToClipboardTextFieldButton); controlPanel.add(columnCountTextFieldLabel); controlPanel.add(columnCountTextField); controlPanel.add(columnCountTextFieldButton); controlPanel.add(paddingTextFieldLabel); controlPanel.add(paddingTextField); controlPanel.add(paddingTextFieldButton); JPanel dataTablePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); dataTablePanel.add(dataTable); this.add(controlPanel, BorderLayout.PAGE_START); this.add(dataTablePanel, BorderLayout.CENTER); dataTable.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { JTable table = (JTable) e.getSource(); TableColumnModel columnModel = table.getColumnModel(); int viewColumn = columnModel.getColumnIndexAtX(e.getX()); int column = columnModel.getColumn(viewColumn).getModelIndex(); if (0 == column) { byteDataTableModel.changeOffsetDisplay(); } } }); } public byte[] getDataCopy() { byte oldData[] = byteDataTableModel.getData(); byte newData[] = new byte[oldData.length]; System.arraycopy(oldData, 0, newData, 0, oldData.length); return newData; } /** * @param message * @param title */ private void displayError(String message, String title) { JOptionPane.showMessageDialog(this.getRootPane(), message, title, JOptionPane.ERROR_MESSAGE); } class ByteDataTableModel extends AbstractTableModel { static public final int ABS_HEX_MODE = 0; static public final int ABS_DECIMAL_MODE = 1; static public final int REL_BASE_HEX_MODE = 2; static public final int REL_BASE_DECIMAL_MODE = 3; static public final int REL_HEX_MODE = 4; static public final int REL_DECIMAL_MODE = 5; private int offsetDisplayMode = ABS_HEX_MODE; static public final int BINARY_MODE = 0; static public final int SIGNED_BYTE_MODE = 1; static public final int CHARACTER_MODE = 2; static public final int OCTAL_MODE = 3; static public final int DECIMAL_MODE = 4; static public final int HEX_MODE = 5; /** * Object array of String, Integer pairs */ public final Object displayModeArray[] = new Object[] { new Object[] { "Binary", new Integer(BINARY_MODE) } , new Object[] { "Signed Byte", new Integer(SIGNED_BYTE_MODE) } , new Object[] { "Character", new Integer(CHARACTER_MODE) } , new Object[] { "Octal", new Integer(OCTAL_MODE) } , new Object[] { "Decimal", new Integer(DECIMAL_MODE) } , new Object[] { "Hex", new Integer(HEX_MODE) } }; public final String displayModeNameArray[] = new String[] { "Binary", "Signed Byte", "Character", "Octal", "Decimal", "Hex" }; private int displayMode = HEX_MODE; private int padding = 0; private int columnCount = -1; private byte byteData[] = null; private int inclusiveStartOffset; private int exclusiveEndOffset; private int intervalLength; private long displayedOffset = 0; public ByteDataTableModel(int columnCount, long baseOffsetForDisplay, byte byteDataArray[], int inclusiveStartOffset, int exclusiveEndOffset) { super(); this.displayedOffset = baseOffsetForDisplay + inclusiveStartOffset; this.inclusiveStartOffset = inclusiveStartOffset; this.exclusiveEndOffset = exclusiveEndOffset; this.intervalLength = exclusiveEndOffset - inclusiveStartOffset; if (intervalLength < columnCount) columnCount = intervalLength; setColumnCount(columnCount + 1); setData(byteDataArray); } public void setPadding(int padding) { this.padding = padding; this.fireTableDataChanged(); } public int getDisplayMode() { return this.displayMode; } public void setDisplayMode(int displayMode) { this.displayMode = displayMode; this.fireTableDataChanged(); } public void setColumnCount(int columnCount) { this.columnCount = columnCount; this.fireTableStructureChanged(); } protected byte[] getData() { return byteData; } public void setData(byte byteDataArray[]) { byteData = byteDataArray; } public String getColumnName(int col) { return String.valueOf(col); } public int getRowCount() { int dataColumnCount = this.columnCount - 1; int effectivePadding = this.padding; if (effectivePadding >= dataColumnCount) effectivePadding = 0; // Often added an extra row // return ((shortData.length + effectivePadding) / (this.columnCount - 1)) + 1; int fullRowsNeeded = (intervalLength + effectivePadding) / (this.columnCount - 1); if ((fullRowsNeeded * (this.columnCount - 1)) < (intervalLength + effectivePadding)) return fullRowsNeeded + 1; else return fullRowsNeeded; } public int getColumnCount() { return this.columnCount; } public String exportDataAsCSV() { String dataString = ""; for (int row = 0; row < getRowCount(); ++row) { for (int col = 0; col < getColumnCount(); ++col) { Object value = getValueAt(row, col); if (value != null) dataString += String.valueOf(value); dataString += "\t"; } dataString += "\n"; } return dataString; } public Object getValueAt(int realRow, int realColumn) { int dataColumn = realColumn - 1; int dataRow = realRow; int numberOfDataColumns = this.columnCount - 1; int effectivePadding = this.padding; if (effectivePadding >= numberOfDataColumns) effectivePadding = 0; if (0 == realColumn) { return getOffsetDisplayString(realRow, realColumn); } if ((effectivePadding > dataColumn) && (0 == realRow)) return null; int index = this.inclusiveStartOffset + (((realRow * numberOfDataColumns) + dataColumn) - effectivePadding); if (index >= this.exclusiveEndOffset) return null; return getDataValueAt(index); } public Object getDataValueAt(int index) { byte aByte = byteData[index]; if (BINARY_MODE == displayMode) { StringBuffer binaryOutput = new StringBuffer(8); for (int i = 7; i >= 0; --i) binaryOutput.append((aByte & 1 << i) != 0 ? '1' : '0'); return binaryOutput.toString(); } else if (SIGNED_BYTE_MODE == displayMode) { return new Byte(aByte); } else if (DECIMAL_MODE == displayMode) { return new Integer(aByte >= 0 ? aByte : (256 + ((int)aByte))); } else if (CHARACTER_MODE == displayMode) { Character ch = new Character((char)(aByte >= 0 ? aByte : (256 + ((int)aByte)))); if (false == Character.isDefined(ch.charValue())) return ".."; if (Character.isISOControl(ch.charValue())) return ".."; return ch; } else if (OCTAL_MODE == displayMode) { return Integer.toOctalString(aByte >= 0 ? aByte : (256 + ((int)aByte))); } else if (HEX_MODE == displayMode) { return Integer.toHexString(aByte >= 0 ? aByte : (256 + ((int)aByte))); } else { displayError("Unsupported Display Mode: " + displayMode, "Error"); return ""; } } public String getOffsetDisplayString(int row, int realColumn) { int dataColumn = realColumn - 1; int dataColumnCount = this.columnCount - 1; int dataColumnOffset = dataColumn; if (dataColumnOffset < 0) dataColumnOffset = 0; int effectivePadding = this.padding; if (effectivePadding >= dataColumnCount) effectivePadding = 0; long offsetValue = dataColumnOffset + (row * dataColumnCount) - effectivePadding; if ((ABS_DECIMAL_MODE == offsetDisplayMode) || (ABS_HEX_MODE == offsetDisplayMode)) { offsetValue += displayedOffset; } if ((REL_BASE_DECIMAL_MODE == offsetDisplayMode) || (REL_BASE_HEX_MODE == offsetDisplayMode)) { offsetValue += this.inclusiveStartOffset; } if (offsetValue < 0) return null; String offsetString = null; if ((ABS_DECIMAL_MODE == offsetDisplayMode) || (REL_BASE_DECIMAL_MODE == offsetDisplayMode) || (REL_DECIMAL_MODE == offsetDisplayMode)) { offsetString = String.valueOf(offsetValue); } else if ((ABS_HEX_MODE == offsetDisplayMode) || (REL_BASE_HEX_MODE == offsetDisplayMode) || (REL_HEX_MODE == offsetDisplayMode)) { offsetString = "0x" + Long.toHexString(offsetValue); } if ((REL_HEX_MODE == offsetDisplayMode) || (REL_DECIMAL_MODE == offsetDisplayMode)) { offsetString += "(r)"; } if ((REL_BASE_HEX_MODE == offsetDisplayMode) || (REL_BASE_DECIMAL_MODE == offsetDisplayMode)) { offsetString += "(b)"; } return offsetString + ":"; } public boolean isCellEditable(int row, int realColumn) { if (0 == realColumn) return false; int dataColumn = realColumn - 1; int dataColumnCount = this.columnCount - 1; int effectivePadding = this.padding; if (effectivePadding >= dataColumnCount) effectivePadding = 0; if ((effectivePadding > dataColumn) && (0 == row)) return false; int index = this.inclusiveStartOffset + (((row * dataColumnCount) + dataColumn) - effectivePadding); if (index >= this.exclusiveEndOffset) return false; return true; } public void setValueAt(Object value, int realRow, int realColumn) { if (0 == realColumn) { displayError("Illegal row,column pair [" + realRow + "," + realColumn + "]", "Error"); return; } int dataColumn = realColumn - 1; int dataColumnCount = this.columnCount - 1; int effectivePadding = this.padding; if (effectivePadding >= dataColumnCount) effectivePadding = 0; if ((effectivePadding > dataColumn) && (0 == realRow)) return; int index = this.inclusiveStartOffset + (((realRow * dataColumnCount) + dataColumn) - effectivePadding); if (index >= this.exclusiveEndOffset) { displayError("Illegal row,column pair [" + realRow + "," + realColumn + "]", "Error"); return; } setDataValueAt(value, index); fireTableCellUpdated(realRow, realColumn); } public void setDataValueAt(Object value, int index) { byte newByteValue = 0; if (value instanceof Number) { newByteValue = ((Number)value).byteValue(); } else if (value instanceof Character) { newByteValue = (byte)(((Character)value).charValue()); } else if (value instanceof String) { String stringValue = (String)value; int intValue = convertValueFromString(displayMode, stringValue); // Error already reported if (-1 == intValue) return; newByteValue = ByteConversions.convertIntToByte(intValue); } else { displayError("Illegal value class: " + value.getClass().getName(), "Error"); return; } byteData[index] = newByteValue; } protected void changeOffsetDisplay() { if (ABS_HEX_MODE == offsetDisplayMode) offsetDisplayMode = ABS_DECIMAL_MODE; else if (ABS_DECIMAL_MODE == offsetDisplayMode) offsetDisplayMode = REL_BASE_HEX_MODE; else if (REL_BASE_HEX_MODE == offsetDisplayMode) offsetDisplayMode = REL_BASE_DECIMAL_MODE; else if (REL_BASE_DECIMAL_MODE == offsetDisplayMode) offsetDisplayMode = REL_HEX_MODE; else if (REL_HEX_MODE == offsetDisplayMode) offsetDisplayMode = REL_DECIMAL_MODE; else if (REL_DECIMAL_MODE == offsetDisplayMode) offsetDisplayMode = ABS_HEX_MODE; else { displayError("Illegal offsetDisplayMode: " + offsetDisplayMode, "Error"); return; } fireTableDataChanged(); } private int convertValueFromString(int displayMode, String value) { switch(displayMode) { case BINARY_MODE: int binaryValue = -1; try { binaryValue = Integer.parseInt(value, 2); } catch (NumberFormatException exception) { displayError("Illegal value: '" + value + "' is not a binary number", "Error"); return -1; } if ((binaryValue > 255) || (binaryValue < 0)) { displayError("Illegal value: binary number must be between 0 and 11111111", "Error"); return -1; } return binaryValue; case SIGNED_BYTE_MODE: int signedIntValue = -129; try { signedIntValue = Integer.parseInt(value); } catch (NumberFormatException exception) { displayError("Illegal value: '" + value + "' is not a signed byte number", "Error"); return -1; } if ((signedIntValue > 127) || (signedIntValue < -128)) { displayError("Illegal value: signed byte number must be between 0 and 255", "Error"); return -1; } return ByteConversions.convertByteToInt((byte)signedIntValue); case DECIMAL_MODE: int intValue = -1; try { intValue = Integer.parseInt(value); } catch (NumberFormatException exception) { displayError("Illegal value: '" + value + "' is not a decimal number", "Error"); return -1; } if ((intValue > 255) || (intValue < 0)) { displayError("Illegal value: decimal number must be between 0 and 255", "Error"); return -1; } return intValue; case CHARACTER_MODE: if (value.length() != 1) { displayError("Illegal value: only one character allowed", "Error"); return -1; } return (byte)(value.charAt(0)); case OCTAL_MODE: int octalValue = -1; try { octalValue = Integer.parseInt(value, 8); } catch (NumberFormatException exception) { displayError("Illegal value: '" + value + "' is not a octal number", "Error"); return -1; } if ((octalValue > 255) || (octalValue < 0)) { displayError("Illegal value: octal number must be between 0 and 377", "Error"); return -1; } return octalValue; case HEX_MODE: int hexValue = -1; try { hexValue = Integer.parseInt(value, 16); } catch (NumberFormatException exception) { displayError("Illegal value: '" + value + "' is not a hex number", "Error"); return -1; } if ((hexValue > 255) || (hexValue < 0)) { displayError("Illegal value: hex number must be between 0 and ff", "Error"); return -1; } return hexValue; default: displayError("Unsupported Display Mode: " + displayMode, "Error"); return -1; } } } }