package com.onaro.util.jfc.tables.rows; import com.onaro.util.jfc.tables.renderers.HtmlWrappedTableCellRenderer; import com.onaro.sanscreen.client.view.tabular.ITabularColumn; import javax.swing.*; import javax.swing.table.TableModel; import javax.swing.table.TableColumn; import javax.swing.table.TableCellRenderer; import javax.swing.text.StyledDocument; import javax.swing.event.*; import java.util.HashMap; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; import java.awt.*; /** * this class tracks the width of rows and change the height accordingly */ public class VarHeightRowTracker { /** * Registered with any column in the table. When a column's width changes it * marks all the rows as theire height needed to be measured. */ private PropertyChangeListener columnWidthListener; /** * Tells which of the table's column contain StyledDocument. Used * when calculating a row's height to save the time of finding such columns. * Indexed by the column number. Updated when the model changes. */ private boolean[] isStyledColumn; /** * The minimum row height allowed. */ private int minRowHeight; private JTable jTable; private JScrollPane scrollPane; private CellHeights hights = new CellHeights(); public VarHeightRowTracker(JTable ajTable, JScrollPane aScrollPane) { if (ajTable == null) throw new IllegalArgumentException("ajTable"); //$NON-NLS-1$ if (aScrollPane == null) throw new IllegalArgumentException("aScrollPane"); //$NON-NLS-1$ this.jTable = ajTable; this.scrollPane = aScrollPane; TableModel model = ajTable.getModel(); columnWidthListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals("width")) { //$NON-NLS-1$ int col = ((TableColumn)evt.getSource()).getModelIndex(); hights.clearColumn(col); validateViewableRows(); jTable.repaint(); } } }; model.addTableModelListener(new TableModelListener() { public void tableChanged(TableModelEvent e) { if (e.getType() == TableModelEvent.UPDATE) { if (e.getColumn() == TableModelEvent.ALL_COLUMNS || StyledDocument.class.isAssignableFrom(jTable.getColumnClass(e.getColumn()))) { hights.clearAll(); validateViewableRows(); jTable.repaint(); } } } }); aScrollPane.getVerticalScrollBar().getModel().addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { validateViewableRows(); } }); registerAllColumnRenderers(); jTable.getColumnModel().addColumnModelListener(new TableColumnModelListener() { public void columnAdded(TableColumnModelEvent e) { registerRenderers(e, true); } public void columnRemoved(TableColumnModelEvent e) { registerRenderers(e, false); } public void columnMarginChanged(ChangeEvent e) { } public void columnMoved(TableColumnModelEvent e) { } public void columnSelectionChanged(ListSelectionEvent e) { } } ); isStyledColumn = null; } private void validateViewableRows() { DefaultBoundedRangeModel brm = (DefaultBoundedRangeModel)scrollPane.getVerticalScrollBar().getModel(); if( !brm.getValueIsAdjusting() ) { int from = jTable.rowAtPoint(new Point(0,brm.getValue())); int to = jTable.rowAtPoint(new Point(0,brm.getValue()+brm.getExtent())); if( to < from && from >= 0) { to = jTable.getRowCount()-1; } for( int rowNum = from; rowNum <= to; rowNum++ ) { if( rowNum >= 0 ) { validateRowHeight(rowNum); } } } } /** * Overrides the table's getRowHeight() method in order to calculate * the height of a row only when it is needed. * @param row the row number * @return the height of the row */ public int getRowHeight(int row) { validateRowHeight(row); return jTable.getHeight(); } /** * Make sure all the rows (visible or not) are correctly displayed */ public void validateAllRowHeights() { for( int cnt = 0; cnt < jTable.getRowCount(); cnt++ ) { validateRowHeight(cnt); } } /** * Calculates the height of a row only if it wasn't calculated before. It is * using the styled-renderers to measure the row's height. * @param row the row number * @return true if the row height was adjusted */ private boolean validateRowHeight(final int row) { boolean res = false; if (isStyledColumn == null) { isStyledColumn = new boolean[jTable.getModel().getColumnCount()]; for (int col = 0; col < jTable.getModel().getColumnCount(); ++col) { TableCellRenderer renderer = jTable.getCellRenderer(row, col); if (renderer instanceof ITabularColumn) renderer = ((ITabularColumn)renderer).getCellRenderer(); isStyledColumn[col] = renderer instanceof HtmlWrappedTableCellRenderer; } } int[] width = new int[isStyledColumn.length]; for (int col = 0; col < isStyledColumn.length; ++col) { width[col] = jTable.getColumnModel().getColumn(col).getWidth(); } int height = 0; int rowhight = 0; for (int col = 0; col < isStyledColumn.length; ++col) { if (isStyledColumn[col]) { height = hights.getCellHight(row,col); if( height == -1 ) { TableCellRenderer cellRend = jTable.getCellRenderer(row,col); Component rendererComp = jTable.prepareRenderer(cellRend, row, col); if (cellRend instanceof ITabularColumn) cellRend = ((ITabularColumn)cellRend).getCellRenderer(); if( cellRend instanceof HtmlWrappedTableCellRenderer ) { HtmlWrappedTableCellRenderer renderer = (HtmlWrappedTableCellRenderer) cellRend; height = Math.max(height, renderer.getCellHeight(width[col] - 5)); } else { rendererComp.setSize(width[col] - 5, 1); height = Math.max(height, rendererComp.getPreferredSize().height); } hights.addHight( row, col, height ); res = true; } } rowhight = Math.max( rowhight, height ); } rowhight = Math.max(rowhight, minRowHeight); if (rowhight > 0) { jTable.setRowHeight(row, rowhight ); } return res; } /** * Gets the minimal row height. * @return the minimal row height */ public int getMinRowHeight() { return minRowHeight; } /** * Sets the minimum row height. * @param minRowHeight the minimum row height */ public void setMinRowHeight(int minRowHeight) { this.minRowHeight = minRowHeight; } /** * When the table's model changes register the cell renderers and width listener * with new columns and remove them for old columns. * @param e the model change event */ private void registerRenderers(TableColumnModelEvent e, boolean added) { registerRenderers(e.getFromIndex(), e.getToIndex(), added); } private void registerAllColumnRenderers() { int count = jTable.getColumnModel().getColumnCount(); registerRenderers(0,count-1,true); } private void registerRenderers(int fromCol, int toCol, boolean added) { for (int col = fromCol; col <= toCol; ++col) { TableColumn column = jTable.getColumnModel().getColumn(col); if (added) { column.addPropertyChangeListener(columnWidthListener); } else { column.removePropertyChangeListener(columnWidthListener); } } isStyledColumn = null; } static class CellHeights { HashMap data; int minrow; int mincol; int maxrow; int maxcol; public CellHeights() { clearAll(); } public int getCellHight( int row, int col ) { Integer res = data.get( new Key(row,col) ); return res != null ? res.intValue() : -1; } public void clearAll() { data = new HashMap(); minrow = Integer.MAX_VALUE; mincol = Integer.MAX_VALUE; maxrow = Integer.MIN_VALUE; maxcol = Integer.MIN_VALUE; } public void clearRow( int row ) { for( int colcnt = mincol; colcnt <= maxcol; colcnt++ ) { data.remove( new Key(row,colcnt) ); } } public void clearColumn( int col ) { for( int rowcnt = minrow; rowcnt <= maxrow; rowcnt++ ) { data.remove( new Key(rowcnt,col) ); } } public void addHight( int row, int col, int hight ) { data.put( new Key(row,col), Integer.valueOf(hight) ); minrow = Math.min( row, minrow ); maxrow = Math.max( row, maxrow ); mincol = Math.min( col, mincol ); maxcol = Math.max( col, maxcol ); } static class Key { public int row; public int col; public Key(int col, int row) { this.col = col; this.row = row; } public int hashCode() { return col*10000+row; } public boolean equals(Object obj) { if( obj == null ) return false; if( !(obj instanceof Key) ) return false; Key key = (Key)obj; return key.row == row && key.col == col; } } } }