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;
}
}
}
}