//(c) Copyright 2011, Scott Vorthmann. package org.vorthmann.zome.ui; import com.vzome.core.model.Connector; import com.vzome.core.model.Manifestation; import com.vzome.core.model.Panel; import com.vzome.core.model.Strut; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.TreeSet; import javax.swing.BorderFactory; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.SwingUtilities; import javax.swing.border.Border; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import org.vorthmann.ui.Controller; import org.vorthmann.zome.app.impl.PartsController.PartInfo; public class PartsPanel extends JPanel { private static final long serialVersionUID = 1L; public PartsPanel( Controller controller ) { super( new BorderLayout() ); PartsTableModel partsTableModel = new PartsTableModel(); controller .addPropertyListener( partsTableModel ); JTable bomTable = new JTable( partsTableModel ); bomTable .setDefaultRenderer( Color.class, new ColorRenderer(true) ); bomTable .setRowSelectionAllowed( false ); bomTable .setCellSelectionEnabled( false ); bomTable .getTableHeader() .setReorderingAllowed( false ); TableColumn column = bomTable .getColumnModel() .getColumn( 0 ); column .setMaxWidth( 50 ); column = bomTable .getColumnModel() .getColumn( 1 ); column .setMaxWidth( 40 ); column = bomTable .getColumnModel() .getColumn( 2 ); column .setMaxWidth( 50 ); JScrollPane bomScroller = new JScrollPane( bomTable ); super.add( bomScroller ); } /** * PartsTableRow allows the PartInfo instances that are received in the PropertyChangeEvent * to be managed as rows and columns in the PartsTableModel. * It also supports the aggregate (total) columns that are generated and used locally. */ public static final class PartsTableRow implements Comparable<PartsTableRow> { private static final int COUNT_COLUMN = 0; private final PartInfo partInfo; private final PartGroupingOrderEnum partClassGroupingOrder; private final Color color; private final Boolean isAutomaticDirection; private final String key; private Integer count = 0; /** * Used internally for generating the initial aggregate rows * @param name Name to be displayed on this aggregate row (e.g. "balls", "struts" or "panels"). * @param partType Class to be counted by this row. * @param initialCount Initial count to be displayed. */ private PartsTableRow(String name, Class<? extends Manifestation> partType, int initialCount) { this( new PartInfo(name, partType), true, initialCount ); } /** * Constructed from the PartInfo from the received in the PropertyChangeEvent * @param partData PartInfo that's received in the PropertyChangeEvent */ private PartsTableRow(PartInfo partData) { this( partData, false, 1 ); } /** * Common c'tor called by the other special purpose c'tors. * @param partData * @param isAggregate * @param initialCount */ private PartsTableRow(PartInfo partData, boolean isAggregate, int initialCount ) { partInfo = partData; partClassGroupingOrder = getPartGroupingOrder(partInfo.partClass, isAggregate); color = new Color(partInfo.rgbColor); isAutomaticDirection = (partInfo.automaticDirectionIndex >= 0); key = calculateKey(partInfo); count = initialCount; } /** * Allows specific internal fields to be addresses as columns * Note the special case of {@code col} = -1 which returns this. * The JPanel doesn't ever use columns less than 0, * but it can be used within this class to allow an individual cell * to access its underlying PartInfo as in the case of the ColorRenderer * @param col Column number (zero based) * @return */ public Object getValueAt(int col) { switch (col) { case -1: return this; // used internally for the color column tool tip text case COUNT_COLUMN: return count; case 1: return color; case 2: return partInfo.sizeNameStr; case 3: return partInfo.lengthStr; } throw new IllegalArgumentException("unexpected column number: " + Integer.toString(col)); } /** * Sort numerically by realLength and automaticDirectionIndex * instead of alphabetically by their String counterparts */ @Override public int compareTo(PartsTableRow that) { int comparison = this.partClassGroupingOrder.compareTo(that.partClassGroupingOrder); if (comparison != 0) return comparison; comparison = this.isAutomaticDirection.compareTo(that.isAutomaticDirection); if (comparison != 0) return comparison; comparison = this.partInfo.automaticDirectionIndex.compareTo(that.partInfo.automaticDirectionIndex); if (comparison != 0) return comparison; comparison = this.partInfo.orbitStr.compareTo(that.partInfo.orbitStr); if (comparison != 0) return comparison; comparison = this.partInfo.realLength.compareTo(that.partInfo.realLength); if (comparison != 0) return comparison; comparison = this.partInfo.sizeNameStr.compareTo(that.partInfo.sizeNameStr); return comparison; } /** * used only by the c'tors * @param partInfo * @return */ private static String calculateKey(PartInfo partInfo) { return partInfo.orbitStr + ":" + partInfo.sizeNameStr + ":" + partInfo.lengthStr; } /** * used only by the c'tors * @param partClass * @param isAggregate * @return */ private static PartGroupingOrderEnum getPartGroupingOrder(Class<? extends Manifestation> partClass, boolean isAggregate ) { if(partClass.equals(Connector.class) && isAggregate ) { return PartGroupingOrderEnum.BALLS_TOTAL; } else if(partClass.equals(Strut.class)) { return isAggregate ? PartGroupingOrderEnum.STRUTS_TOTAL : PartGroupingOrderEnum.STRUTS; } else if(partClass.equals(Panel.class)) { return isAggregate ? PartGroupingOrderEnum.PANELS_TOTAL : PartGroupingOrderEnum.PANELS; } return PartGroupingOrderEnum.TEMP; } } /** * The order listed here is the order the rows will be grouped and sorted for display */ private enum PartGroupingOrderEnum { BALLS_TOTAL, STRUTS_TOTAL, STRUTS, PANELS_TOTAL, PANELS, TEMP; // nothing displayed in this case } private final class PartsTableModel extends AbstractTableModel implements PropertyChangeListener { private static final long serialVersionUID = 1L; private final String[] columnNames = { "count", "orbit", "size", "length" }; private final TreeSet<PartsTableRow> tableRows = new TreeSet<>(); // self-sorting since PartsTableRow implements Comparable private final PartsTableRow ballsTotalRow = new PartsTableRow("balls", Connector.class, 1 ); private final PartsTableRow strutsTotalRow = new PartsTableRow("struts", Strut.class, 0 ); private final PartsTableRow panelsTotalRow = new PartsTableRow("panels", Panel.class, 0 ); PartsTableModel() { tableRows.add(ballsTotalRow); } @Override public Class<?> getColumnClass( int col ) { return ballsTotalRow.getValueAt( col ).getClass(); } @Override public boolean isCellEditable( int row, int col ) { //Note that the data/cell address is constant, //no matter where the cell appears onscreen. return false; } @Override public String getColumnName( int col ) { return columnNames[ col ]; } @Override public int getColumnCount() { return columnNames.length; } @Override public int getRowCount() { return tableRows.size(); } @Override public Object getValueAt( int row, int col ) { PartsTableRow rowData = getRow( row ); return rowData == null ? null : rowData.getValueAt( col ); } public PartsTableRow getRow( int row ) { return tableRows.stream().skip(row).findFirst().orElse(null); } public PartsTableRow getRow( String key ) { return tableRows.stream().filter(r -> r.key.equals(key)).findFirst().orElse(null); } public int getRowNumber( PartsTableRow row ) { return tableRows.contains(row) ? tableRows. headSet(row).size() // the number of elements that are less than row : -1; } @Override public void propertyChange( PropertyChangeEvent event ) { String propertyName = event .getPropertyName(); if( propertyName.equals("addBall") || propertyName.equals("removeBall") || propertyName.equals("addStrut") || propertyName.equals("removeStrut") || propertyName.equals("addPanel") || propertyName.equals("removePanel") ) { ManifestationChangeHandler handler = new ManifestationChangeHandler(event); if (SwingUtilities.isEventDispatchThread()) { handler.run(); } else { SwingUtilities.invokeLater(handler); } } } private class ManifestationChangeHandler implements Runnable { private final String propertyName; private final PartsTableRow oldRow; private final PartsTableRow newRow; public ManifestationChangeHandler(PropertyChangeEvent event) { // Don't maintain a reference to the actual event. // Just copy the data to be used later in another thread. propertyName = event.getPropertyName(); PartInfo oldPart = (PartInfo) event.getOldValue(); PartInfo newPart = (PartInfo) event.getNewValue(); oldRow = oldPart == null ? null : new PartsTableRow(oldPart); newRow = newPart == null ? null : new PartsTableRow(newPart); } @Override public void run() { switch(propertyName) { case "addBall": incrementAggregateRow(ballsTotalRow, false); break; case "removeBall": decrementAggregateRow(ballsTotalRow, false); break; case "addStrut": incrementAggregateRow(strutsTotalRow, true); updateOrInsertNewRow(); break; case "removeStrut": decrementAggregateRow(strutsTotalRow, true); updateOrRemoveOldRow(); break; case "addPanel": incrementAggregateRow(panelsTotalRow, true); updateOrInsertNewRow(); break; case "removePanel": decrementAggregateRow(panelsTotalRow, true); updateOrRemoveOldRow(); break; } } private void incrementAggregateRow(PartsTableRow aggregateRow, boolean addInitial ) { if (addInitial) { updateOrInsertRow(aggregateRow); } else { aggregateRow.count++; int row = getRowNumber(aggregateRow); fireTableCellUpdated(row, PartsTableRow.COUNT_COLUMN); } } private void decrementAggregateRow(PartsTableRow tableRow, boolean removeEmpty ) { if (removeEmpty) { updateOrDeleteRow(tableRow); } else { tableRow.count--; int row = getRowNumber(tableRow); fireTableCellUpdated(row, PartsTableRow.COUNT_COLUMN); } } private void updateOrInsertNewRow() { updateOrInsertRow(newRow); } private void updateOrRemoveOldRow() { updateOrDeleteRow(oldRow); } private void updateOrInsertRow(PartsTableRow insertRow) { PartsTableRow existingRow = getRow(insertRow.key); if (existingRow == null) { insertRow.count = 1; tableRows.add(insertRow); // determine rowNumber AFTER adding insertedRow int rowNumber = getRowNumber(insertRow); fireTableRowsInserted(rowNumber, rowNumber); } else { existingRow.count++; int row = getRowNumber(existingRow); fireTableCellUpdated(row, PartsTableRow.COUNT_COLUMN); } } private void updateOrDeleteRow(PartsTableRow deleteRow) { PartsTableRow existingRow = getRow(deleteRow.key); existingRow.count--; // existingRow should never be null if (existingRow.count == 0) { // determine rowNumber BEFORE removing deleteRow int rowNumber = getRowNumber(existingRow); tableRows.remove(existingRow); fireTableRowsDeleted(rowNumber, rowNumber); } else { int row = getRowNumber(existingRow); fireTableCellUpdated(row, PartsTableRow.COUNT_COLUMN); } } } } private final class ColorRenderer extends JLabel implements TableCellRenderer { private static final long serialVersionUID = 1L; Border unselectedBorder = null; Border selectedBorder = null; boolean isBordered = true; public ColorRenderer( boolean isBordered ) { this.isBordered = isBordered; super.setOpaque(true); // MUST do this for background to show up. } @Override public Component getTableCellRendererComponent( JTable table, Object color, boolean isSelected, boolean hasFocus, int row, int column) { Color newColor = (Color)color; setBackground(newColor); if (isBordered) { if (isSelected) { if (selectedBorder == null) { selectedBorder = BorderFactory.createMatteBorder(2,5,2,5, table.getSelectionBackground()); } setBorder(selectedBorder); } else { if (unselectedBorder == null) { unselectedBorder = BorderFactory.createMatteBorder(2,5,2,5, table.getBackground()); } setBorder(unselectedBorder); } } PartsTableRow rowData = (PartsTableRow) table.getValueAt(row, -1); if( rowData.partClassGroupingOrder == PartGroupingOrderEnum.PANELS || rowData.partClassGroupingOrder == PartGroupingOrderEnum.STRUTS ) { if(rowData.isAutomaticDirection) { setToolTipText("auto " + rowData.partInfo.orbitStr); } else { setToolTipText(rowData.partInfo.orbitStr + " RGB: " + newColor.getRed() + ", " + newColor.getGreen() + ", " + newColor.getBlue() ); } } else { setToolTipText(null); } return this; } } }