ArduinoTableSignal.java
/*******************************************************************************
* jArduino: Arduino C++ Code Generation From Java
* Copyright 2020 Tony Washer
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package net.sourceforge.jarduino.gui;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.AbstractCellEditor;
import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.RowFilter;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableRowSorter;
import net.sourceforge.jarduino.message.ArduinoMessage;
import net.sourceforge.jarduino.message.ArduinoSignal;
import net.sourceforge.jarduino.message.ArduinoSignalDefinition;
import net.sourceforge.jarduino.message.ArduinoSignalFactor;
import net.sourceforge.jarduino.message.ArduinoSignalRange;
import net.sourceforge.jarduino.util.ArduinoField;
/**
* Message Signal Table.
*/
public class ArduinoTableSignal {
/**
* Colors.
*/
private static final Color[] COLORS = {
Color.decode("#FF0000"), Color.decode("#FF7F00"), Color.decode("#FFD400"), Color.decode("#FFFF00"),
Color.decode("#BFFF00"), Color.decode("#6AFF00"), Color.decode("#00EAFF"), Color.decode("#0095FF"),
Color.decode("#0040FF"), Color.decode("#AA00FF"), Color.decode("#FF00AA"), Color.decode("#EDB9B9"),
Color.decode("#E7E9B9"), Color.decode("#B9EDE0"), Color.decode("#B9D7ED"), Color.decode("#DCB9ED"),
Color.decode("#8F2323"), Color.decode("#8F6A23"), Color.decode("#4F8F23"), Color.decode("#23628F"),
Color.decode("#6B238F"), Color.decode("#000000"), Color.decode("#737373"), Color.decode("#CCCCCC")
};
/**
* Normal border.
*/
private static final Border BORDER_NORMAL = new LineBorder(Color.black);
/**
* Error border.
*/
private static final Border BORDER_ERROR = new LineBorder(Color.red);
/**
* Rounding factor (add 0.5 to double before casting to long to get rounding right).
*/
private static final double ROUNDING = 0.5;
/**
* Small width.
*/
private static final int WIDTH_SMALL = 20;
/**
* Medium width.
*/
private static final int WIDTH_MEDIUM = 40;
/**
* Large width.
*/
private static final int WIDTH_LARGE = 150;
/**
* The owning panel.
*/
private final ArduinoPanelSignal theOwner;
/**
* The Message table.
*/
private final JTable theTable;
/**
* The signals list.
*/
private final List<ArduinoSignal> theSignals;
/**
* The bitMaps list.
*/
private final List<byte[]> theBitMaps;
/**
* The values Map.
*/
private final Map<ArduinoSignal, Object> theValues;
/**
* The colours Map.
*/
private final Map<ArduinoSignal, Color> theColours;
/**
* The model.
*/
private final ArduinoSignalModel theModel;
/**
* The Multiplex column.
*/
private final TableColumn theMultiCol;
/**
* Is the multiplex column visible?
*/
private boolean multiVisible;
/**
* The selected message.
*/
private ArduinoMessage theMessage;
/**
* Constructor.
*
* @param pOwner the owning panel
*/
ArduinoTableSignal(final ArduinoPanelSignal pOwner) {
/* Store owner */
theOwner = pOwner;
/* Create components */
theTable = new JTable();
/* Create the lists and maps */
theSignals = new ArrayList<>();
theBitMaps = new ArrayList<>();
theValues = new HashMap<>();
theColours = new HashMap<>();
/* Create the table model */
theModel = new ArduinoSignalModel();
theTable.setModel(theModel);
/* Set the row filter */
final TableRowSorter<ArduinoSignalModel> mySorter = new TableRowSorter<>(theModel);
mySorter.setRowFilter(new MultiFilter());
theTable.setRowSorter(mySorter);
/* Configure the columns */
theMultiCol = configureColumns();
multiVisible = true;
}
/**
* Configure the columns.
* @return the multiplex column
*/
private TableColumn configureColumns() {
/* Create renderers */
final TableCellRenderer myRightRenderer = new AlignmentRenderer(SwingConstants.RIGHT);
final TableCellRenderer myCenterRenderer = new AlignmentRenderer(SwingConstants.CENTER);
final TableCellRenderer myColorRenderer = new ColorRenderer();
final TableCellEditor myCellEditor = new DataCellEditor();
/* Access the column model */
final TableColumnModel myModel = theTable.getColumnModel();
/* Set the signal column */
TableColumn myColumn = myModel.getColumn(ArduinoSignalModel.COL_SIGNAL);
myColumn.setPreferredWidth(WIDTH_LARGE);
myColumn.setCellRenderer(myRightRenderer);
/* Set the value column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_VALUE);
myColumn.setPreferredWidth(WIDTH_MEDIUM);
myColumn.setCellRenderer(myCenterRenderer);
myColumn.setCellEditor(myCellEditor);
/* Set the color column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_COLOR);
myColumn.setPreferredWidth(WIDTH_MEDIUM);
myColumn.setMaxWidth(WIDTH_MEDIUM);
myColumn.setCellRenderer(myColorRenderer);
/* Set the startBit column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_START);
myColumn.setPreferredWidth(WIDTH_SMALL);
myColumn.setMaxWidth(WIDTH_SMALL);
myColumn.setCellRenderer(myCenterRenderer);
/* Set the length column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_LENGTH);
myColumn.setPreferredWidth(WIDTH_SMALL);
myColumn.setMaxWidth(WIDTH_SMALL);
myColumn.setCellRenderer(myCenterRenderer);
/* Set the endian column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_ENDIAN);
myColumn.setPreferredWidth(WIDTH_SMALL);
myColumn.setMaxWidth(WIDTH_SMALL);
myColumn.setCellRenderer(myCenterRenderer);
/* Set the units column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_UNITS);
myColumn.setPreferredWidth(WIDTH_MEDIUM);
myColumn.setCellRenderer(myCenterRenderer);
/* Set the factor column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_FACTOR);
myColumn.setPreferredWidth(WIDTH_MEDIUM);
myColumn.setCellRenderer(myCenterRenderer);
/* Set the offset column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_OFFSET);
myColumn.setPreferredWidth(WIDTH_MEDIUM);
myColumn.setCellRenderer(myCenterRenderer);
/* Set the min column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_MIN);
myColumn.setPreferredWidth(WIDTH_MEDIUM);
myColumn.setCellRenderer(myCenterRenderer);
/* Set the max column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_MAX);
myColumn.setPreferredWidth(WIDTH_MEDIUM);
myColumn.setCellRenderer(myCenterRenderer);
/* Handle the multiplex column */
myColumn = myModel.getColumn(ArduinoSignalModel.COL_MULTI);
myColumn.setPreferredWidth(WIDTH_SMALL);
myColumn.setCellRenderer(myCenterRenderer);
return myColumn;
}
/**
* Obtain the component.
*
* @return the component
*/
JComponent getComponent() {
return theTable;
}
/**
* Obtain color for index.
*
* @param pIndex the index
* @return the colour
*/
private static Color getColourForIndex(final int pIndex) {
return COLORS[pIndex % COLORS.length];
}
/**
* Configure the table.
*
* @param pMessage the message
*/
void configureTable(final ArduinoMessage pMessage) {
/* store the message */
theMessage = pMessage;
/* Refresh the table */
theModel.refresh();
/* Determine whether multiplex column should be visible */
final boolean bMultiVisible = theMessage.hasMultiplex();
/* If we should modify the columns */
if (bMultiVisible != multiVisible) {
/* Access the table column model */
final TableColumnModel myColModel = theTable.getColumnModel();
/* If we need to remove the column */
if (multiVisible) {
myColModel.removeColumn(theMultiCol);
/* else restore the column and move to correct position */
} else {
myColModel.addColumn(theMultiCol);
myColModel.moveColumn(ArduinoSignalModel.COL_MAX, ArduinoSignalModel.COL_MULTI);
}
/* Flip the flag */
multiVisible = !multiVisible;
}
}
/**
* Obtain number of rows.
*
* @return the the number of rows
*/
int getNumRows() {
return theSignals.size();
}
/**
* Obtain color for row.
*
* @param pRowIndex the row index
* @return the color
*/
Color getColorForRow(final int pRowIndex) {
final ArduinoSignal mySignal = theSignals.get(pRowIndex);
return theColours.get(mySignal);
}
/**
* Obtain bitmap for row.
*
* @param pRowIndex the row index
* @return the bitMap
*/
byte[] getBitMapForRow(final int pRowIndex) {
return theBitMaps.get(pRowIndex);
}
/**
* loadValues.
*/
void loadValues() {
/* Clear the maps */
theValues.clear();
theColours.clear();
/* Loop through the signals */
Long myMultiplex = ArduinoSignal.MULTI_NONE;
int myColorId = 0;
for (ArduinoSignal mySignal : theSignals) {
/* If the value is relevant */
final Number myMId = mySignal.getMultiplexId();
if (myMId.equals(ArduinoSignal.MULTI_NONE)
|| myMId.equals(myMultiplex)) {
/* Access the value and store into map */
final Number myValue = getValueForSignal(mySignal);
theValues.put(mySignal, myValue);
/* Store colour for index */
theColours.put(mySignal, getColourForIndex(myColorId++));
/* Store multiplex value if we have it */
if (mySignal.isMultiplex()) {
myMultiplex = myValue.longValue();
}
}
}
/* Update the table */
if (theModel != null) {
theModel.fireTableDataChanged();
}
}
/**
* Obtain value for signal.
*
* @param pSignal the signal
* @return the value
*/
private Number getValueForSignal(final ArduinoSignal pSignal) {
/* Access the data buffer */
final byte[] myData = theOwner.getData();
/* Access the raw value */
final ArduinoSignalDefinition myDef = pSignal.getDefinition();
final long myValue = myDef.getDataType().isLittleEndian()
? ArduinoField.parseLEField(myData, myDef.getStartBit(), myDef.getBitLength(), myDef.getDataType().isSigned())
: ArduinoField.parseBEField(myData, myDef.getStartBit(), myDef.getBitLength(), myDef.getDataType().isSigned());
/* Apply the adjustment */
final ArduinoSignalFactor myFactor = pSignal.getFactor();
return myFactor.isFloat()
? (Number) (myFactor.getFactor().doubleValue() * myValue + myFactor.getOffset().doubleValue())
: (Number) (myFactor.getFactor().longValue() * myValue + myFactor.getOffset().longValue());
}
/**
* Table Model.
*/
private class ArduinoSignalModel
extends AbstractTableModel {
/**
* Serial Id.
*/
private static final long serialVersionUID = 3030910683228702589L;
/**
* The signal column.
*/
private static final int COL_SIGNAL = 0;
/**
* The value column.
*/
private static final int COL_VALUE = COL_SIGNAL + 1;
/**
* The startBits column.
*/
private static final int COL_START = COL_VALUE + 1;
/**
* The length column.
*/
private static final int COL_LENGTH = COL_START + 1;
/**
* The endian column.
*/
private static final int COL_ENDIAN = COL_LENGTH + 1;
/**
* The multiplex column.
*/
private static final int COL_MULTI = COL_ENDIAN + 1;
/**
* The color column.
*/
private static final int COL_COLOR = COL_MULTI + 1;
/**
* The units column.
*/
private static final int COL_UNITS = COL_COLOR + 1;
/**
* The factor column.
*/
private static final int COL_FACTOR = COL_UNITS + 1;
/**
* The offset column.
*/
private static final int COL_OFFSET = COL_FACTOR + 1;
/**
* The minimum column.
*/
private static final int COL_MIN = COL_OFFSET + 1;
/**
* The maximum column.
*/
private static final int COL_MAX = COL_MIN + 1;
/**
* Constructor.
*/
ArduinoSignalModel() {
}
/**
* Refresh.
*/
void refresh() {
/* Clear the signals */
theSignals.clear();
/* Add the signals */
theSignals.addAll(theMessage.getAllSignals());
/* load the values and bitMaps */
loadValues();
loadBitMaps();
/* Refresh the table */
fireTableDataChanged();
}
/**
* loadBitMaps.
*/
private void loadBitMaps() {
/* Clear the maps */
theBitMaps.clear();
/* Loop through the signals */
for (ArduinoSignal mySignal : theSignals) {
/* Access the value and store into map */
final byte[] myBitMap = getBitMapForSignal(mySignal);
theBitMaps.add(myBitMap);
}
}
@Override
public int getRowCount() {
return theSignals.size();
}
@Override
public String getColumnName(final int pColIndex) {
switch (pColIndex) {
case COL_SIGNAL:
return "Signal";
case COL_VALUE:
return "Value";
case COL_START:
return "S";
case COL_LENGTH:
return "L";
case COL_ENDIAN:
return "E";
case COL_MULTI:
return "M";
case COL_COLOR:
return "Color";
case COL_UNITS:
return "Units";
case COL_FACTOR:
return "Factor";
case COL_OFFSET:
return "Offset";
case COL_MIN:
return "Min";
case COL_MAX:
return "Max";
default:
return null;
}
}
@Override
public int getColumnCount() {
return COL_MAX + 1;
}
@Override
public boolean isCellEditable(final int pRowIndex,
final int pColIndex) {
final ArduinoSignal mySignal = theSignals.get(pRowIndex);
return pColIndex == COL_VALUE && theValues.get(mySignal) != null;
}
@Override
public Object getValueAt(final int pRowIndex,
final int pColIndex) {
final ArduinoSignal mySignal = theSignals.get(pRowIndex);
final ArduinoSignalDefinition myDef = mySignal.getDefinition();
final ArduinoSignalFactor myFactor = mySignal.getFactor();
final ArduinoSignalRange myRange = mySignal.getRange();
final long myMId = mySignal.getMultiplexId();
switch (pColIndex) {
case COL_SIGNAL:
return mySignal.getName();
case COL_VALUE:
return theValues.get(mySignal);
case COL_START:
return myDef.getStartBit();
case COL_LENGTH:
return myDef.getBitLength();
case COL_ENDIAN:
return myDef.getDataType().isLittleEndian()
? "L"
: "B";
case COL_MULTI:
if (mySignal.isMultiplex()) {
return "M";
}
return myMId == -1
? null
: myMId;
case COL_COLOR:
return theColours.get(mySignal);
case COL_UNITS:
return mySignal.getUnits();
case COL_FACTOR:
return myFactor.getFactor();
case COL_OFFSET:
return myFactor.getOffset();
case COL_MIN:
return myRange.unBounded()
? null
: myRange.getMinimum();
case COL_MAX:
return myRange.unBounded()
? null
: myRange.getMaximum();
default:
return null;
}
}
@Override
public void setValueAt(final Object pValue,
final int pRowIndex,
final int pColIndex) {
/* Access signal and set value */
final ArduinoSignal mySignal = theSignals.get(pRowIndex);
setValueForSignal(mySignal, (Number) pValue);
/* reload values */
loadValues();
/* Update the hex and binary */
theOwner.updateHexAndBinary();
theOwner.updateBitColors();
}
/**
* Obtain bitMap for signal.
*
* @param pSignal the signal
* @return the value
*/
private byte[] getBitMapForSignal(final ArduinoSignal pSignal) {
/* Access the data buffer */
final byte[] myData = new byte[theMessage.getLength()];
/* Access the raw value */
final ArduinoSignalDefinition myDef = pSignal.getDefinition();
if (myDef.getDataType().isLittleEndian()) {
ArduinoField.setLEFieldBits(myData, myDef.getStartBit(), myDef.getBitLength());
} else {
ArduinoField.setBEFieldBits(myData, myDef.getStartBit(), myDef.getBitLength());
}
/* Return the bytes */
return myData;
}
/**
* Set value for signal.
*
* @param pSignal the signal
* @param pValue the value
*/
private void setValueForSignal(final ArduinoSignal pSignal,
final Number pValue) {
/* Access the data buffer */
final byte[] myData = theOwner.getData();
/* Apply the adjustment */
final ArduinoSignalFactor myFactor = pSignal.getFactor();
final long myValue = myFactor.isFloat()
? (long) (ROUNDING + ((pValue.doubleValue() - myFactor.getOffset().doubleValue()) / myFactor.getFactor().doubleValue()))
: (pValue.longValue() - myFactor.getOffset().longValue()) / myFactor.getFactor().longValue();
/* Set the raw value */
final ArduinoSignalDefinition myDef = pSignal.getDefinition();
if (myDef.getDataType().isLittleEndian()) {
ArduinoField.setLEField(myData, myDef.getStartBit(), myDef.getBitLength(), myValue);
} else {
ArduinoField.setBEField(myData, myDef.getStartBit(), myDef.getBitLength(), myValue);
}
}
}
/**
* Color Renderer.
*/
private static class ColorRenderer extends DefaultTableCellRenderer {
@Override
public void setValue(final Object pValue) {
setBackground((Color) pValue);
}
}
/**
* Simple Alignment Renderer.
*/
private class AlignmentRenderer extends DefaultTableCellRenderer {
/**
* Serial Id.
*/
private static final long serialVersionUID = -6387123991399458414L;
/**
* Number format.
*/
private final NumberFormat myFormatter = NumberFormat.getInstance();
/**
* Constructor.
*
* @param pAlignment the alignment
*/
AlignmentRenderer(final int pAlignment) {
setHorizontalAlignment(pAlignment);
}
@Override
public JComponent getTableCellRendererComponent(final JTable pTable,
final Object pValue,
final boolean pSelected,
final boolean hasFocus,
final int pRow,
final int pCol) {
/* Determine the active signal */
final int myRow = pTable.convertRowIndexToModel(pRow);
final ArduinoSignal mySignal = theSignals.get(myRow);
/* Obtain the string representation */
String myValue = pValue == null
? null
: pValue.toString();
/* If this is a double */
if (pValue instanceof Double) {
/* Find out how many decimals we have */
final int myNumDecimals = mySignal.getFactor().getNumDecimals();
myFormatter.setMaximumFractionDigits(myNumDecimals);
myFormatter.setMinimumFractionDigits(myNumDecimals);
/* Truncate the decimals */
myValue = myFormatter.format(((Double) pValue).doubleValue());
}
/* Set the value */
setText(myValue);
/* return this as the component */
return this;
}
}
/**
* Data Cell Editor.
*/
private class DataCellEditor
extends AbstractCellEditor
implements TableCellEditor {
/**
* Serial Id.
*/
private static final long serialVersionUID = 7142514324836088035L;
/**
* Are we in the middle of stopping edit?
*/
private final JTextField theEditor;
/**
* Number format.
*/
private final NumberFormat myFormatter = NumberFormat.getInstance();
/**
* The signal.
*/
private transient ArduinoSignal theSignal;
/**
* The edited value.
*/
private Number theValue;
/**
* Are we in the middle of stopping edit?
*/
private boolean stoppingEdit;
/**
* Constructor.
*/
DataCellEditor() {
theEditor = new JTextField();
theEditor.setHorizontalAlignment(SwingConstants.CENTER);
theEditor.addFocusListener(new FocusListener() {
@Override
public void focusLost(final FocusEvent pE) {
/* Do nothing */
}
@Override
public void focusGained(final FocusEvent pE) {
theEditor.selectAll();
}
});
}
@Override
public Number getCellEditorValue() {
return theValue;
}
@Override
public Component getTableCellEditorComponent(final JTable pTable,
final Object pValue,
final boolean pSelected,
final int pRow,
final int pCol) {
/* Determine the active signal */
final int myRow = pTable.convertRowIndexToModel(pRow);
theSignal = theSignals.get(myRow);
/* Format the value */
String myValue = null;
if (pValue instanceof Double) {
/* Find out how many decimals we have */
final int myNumDecimals = theSignal.getFactor().getNumDecimals();
myFormatter.setMaximumFractionDigits(myNumDecimals);
myFormatter.setMinimumFractionDigits(myNumDecimals);
myValue = myFormatter.format(((Number) pValue).doubleValue());
} else if (pValue instanceof Long) {
myValue = pValue.toString();
}
/* Set field value and start edit */
theEditor.setText(myValue);
theEditor.setBorder(BORDER_NORMAL);
/* Return the field */
return theEditor;
}
@Override
public boolean stopCellEditing() {
/* If we are already stopping edit */
if (stoppingEdit) {
return true;
}
/* Parse the data */
stoppingEdit = true;
try {
theValue = myFormatter.parse(theEditor.getText());
/* Catch parse exceptions */
} catch (ParseException e) {
/* Return status and set error border */
stoppingEdit = false;
theEditor.setBorder(BORDER_ERROR);
return false;
}
/* Make sure that the value is the correct dataType */
if (theValue instanceof Long && theSignal.isFloat()) {
theValue = theValue.doubleValue();
}
if (theValue instanceof Double && !theSignal.isFloat()) {
theValue = theValue.longValue();
}
/* If there is no error */
boolean bComplete = false;
if (checkValueForSignal(theSignal, theValue)) {
/* Pass call onwards */
bComplete = super.stopCellEditing();
/* else set error border */
} else {
theEditor.setBorder(BORDER_ERROR);
}
/* Return status */
stoppingEdit = false;
return bComplete;
}
/**
* Check value for signal.
*
* @param pSignal the signal
* @param pValue the value
* @return valid true/false
*/
private boolean checkValueForSignal(final ArduinoSignal pSignal,
final Number pValue) {
/* If we are unbounded, just return OK */
final ArduinoSignalRange myRange = pSignal.getRange();
if (myRange.unBounded()) {
return true;
}
/* Check the value */
return pValue instanceof Double
? pValue.doubleValue() >= myRange.getMinimum().doubleValue()
&& pValue.doubleValue() <= myRange.getMaximum().doubleValue()
: pValue.longValue() >= myRange.getMinimum().longValue()
&& pValue.longValue() <= myRange.getMaximum().longValue();
}
}
/**
* RowFilter class.
*/
private class MultiFilter extends RowFilter<ArduinoSignalModel, Integer> {
@Override
public boolean include(final Entry<? extends ArduinoSignalModel, ? extends Integer> entry) {
/* Always show if this message has no multiplex */
final ArduinoSignal myMulti = theMessage.getMultiplexSignal();
if (myMulti == null) {
return true;
}
/* Access the signal and show if it is non-multiplex or matches the multiplex */
final ArduinoSignal mySignal = theSignals.get(entry.getIdentifier());
final Long myMId = mySignal.getMultiplexId();
return myMId.equals(ArduinoSignal.MULTI_NONE)
|| myMId.equals(theValues.get(myMulti));
}
}
}