package net.xoetrope.awt;
import java.awt.AWTEventMulticaster;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.ItemSelectable;
import java.awt.ScrollPane;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import net.xoetrope.xui.XProjectManager;
import net.xoetrope.xui.data.XDataBinding;
import net.xoetrope.xui.data.XModel;
import net.xoetrope.xui.data.XTextBinding;
import net.xoetrope.xui.style.XStyle;
import java.awt.Rectangle;
import java.util.Hashtable;
/**
* <p>Provides a simple read-only tables/grid component.</p>
* <p>Copyright (c) Xoetrope Ltd., 1998-2004<br>
* License: see license.txt
* $Revision: 2.3 $
*/
public class XTableRenderer extends Canvas implements MouseListener, KeyListener, ItemSelectable
{
private int[] colWidth;
private int colPadding = 2;
private int currentY = 0;
private int itemIdx;
private XModel model;
private Font font;
private FontMetrics fontMetrics;
private int fontHeight;
private int rowHeight;
private int headerHeight;
private String tableStyle, headerStyle, selectedStyle;
private Color backColor, foreColor, darkerColor;
private static final double darkerFactor = 0.95;
private boolean updateModelSelection;
private boolean interactiveTable;
private boolean drawFrame;
private int selectedRow;
private int selectedColumn;
private int startRow;
private XTable owner;
private boolean drawBorder;
private Color borderColor;
private static final int VK_KP_UP = 0xE0;
private static final int VK_KP_DOWN = 0xE1;
// private static final int VK_KP_LEFT = 0xE2;
// private static final int VK_KP_RIGHT = 0xE3;
private Object[] components;
private Component currentComponent = null;
private XDataBinding editBinding;
private boolean rendered = false;
/**
* Create a new table renderer
* @param parent the parent container/table
*/
public XTableRenderer( XTable parent )
{
interactiveTable = false;
drawFrame = false;
selectedRow = 0;
selectedColumn = 0;
startRow = 0;
fontHeight = 13;
addMouseListener( this );
addKeyListener( this );
owner = parent;
}
/**
* Set the XModel which we will be generating the table from
* @param xmodel The XModel of data
*/
public void setContent( XModel xmodel )
{
model = xmodel;
if ( model != null ) {
int numChildren = model.get( 0 ).getNumChildren();
components = new Object[ numChildren ];
colWidth = new int[ numChildren ];
for ( int i = 0; i < colWidth.length; i++ )
colWidth[ i ] = 100;
}
else
components = null;
}
/**
* Set the type of component for a column
* @param col the field index
* @param compName the component name
*/
public void setComponentAt( int col, String compName )
{
components[ col ] = compName;
}
/**
* Set the general style of the XTable
* @param style XStyle
*/
public void setStyle( String style )
{
if ( style == null )
tableStyle = "base";
else
tableStyle = style;
}
/**
* Set the style of the header data
* @param style XStyle
*/
public void setHeaderStyle( String style )
{
headerStyle = style;
}
/**
* Set the style of the selected row
* @param style XStyle
*/
public void setSelectedStyle( String style )
{
selectedStyle = style;
}
/**
* Set the style for the border
* @param styleName the style name
*/
public void setBorderStyle( String styleName )
{
XStyle style = XProjectManager.getStyleManager().getStyle( styleName );
borderColor = style.getStyleAsColor( XStyle.COLOR_FORE );
drawBorder = true;
}
/**
* Check the if the table is interactive
* @return true if the table supports user interaction
*/
public boolean isInteractiveTable()
{
return interactiveTable;
}
/**
* Set the user interaction state
* @param state true for an user interactive table.
*/
public void setInteractiveTable( boolean state )
{
interactiveTable = state;
repaint();
}
/**
* Draw an individual row of data. Alternate the backcolor for every other
* row. Set the clip region and fill the background. Loop each XModel in
* the model parameter drawing the values and drawing a vertical line to the
* right of the cell. Draw a line under the row
* @param row The index of the row being rendered
* @param g The Graphics object
* @param model The XModel containing the row of data
*/
private void renderRow( int row, Graphics g, XModel model, boolean headerCell )
{
int currentX = drawFrame ? 1 : 0 ;
clearRow( g, row );
for ( int fieldIdx = 0; fieldIdx < model.getNumChildren(); fieldIdx++ ){
if ( model.get( fieldIdx )!=null ){
try {
currentX = renderCell( g, currentX, model.get( fieldIdx ), (String)model.get( fieldIdx ).get(), fieldIdx, headerCell );
}
catch ( Exception ex ) {
ex.printStackTrace();
}
}
}
}
/**
* Render a table cell
* @param g The graphics context
* @param currentX the X position
* @param value the value to render
* @param fieldIdx the field index
* @param headerCell true if a header cell is being rendered
* @return the new X position
*/
private int renderCell( Graphics g, int currentX, XModel model, String value, int fieldIdx, boolean headerCell )
{
Rectangle clipRect = g.getClipBounds();
int clipY = currentY - fontMetrics.getAscent() - colPadding;
int clipW = clipRect.width - ( currentX - clipRect.x );
int clipH = Math.min( clipRect.height - ( clipY - clipRect.y ), getSize().height );
g.setColor( foreColor );
g.setClip( currentX, clipY, Math.min( colWidth[ fieldIdx ] - ( colPadding * 2 ), clipW ), clipH );
if ( value != null )
g.drawString( value, currentX + colPadding, currentY );
g.setColor( borderColor );
g.setClip( currentX, clipY, Math.min( getSize().width, clipW ), clipH );
if ( drawBorder )
g.drawRect( currentX
, currentY - fontMetrics.getAscent() - colPadding
, colWidth[ fieldIdx ] - 1
, fontHeight + ( colPadding * 2 ));
// Draw a vertical line to the right of the cell
g.drawLine( currentX + colWidth[ fieldIdx ] - 1
, currentY - fontMetrics.getAscent() - colPadding
, currentX + colWidth[ fieldIdx ] - 1
, currentY + ( colPadding * 2 ) + 1 );
currentX += colWidth[ fieldIdx ];
g.setClip( clipRect );
return currentX;
}
/**
* Erase the backgroudn of a row
* @param g The graphics context
* @param row the row number
*/
private void clearRow( Graphics g, int row )
{
if ( row % 2 == 1 )
g.setColor( darkerColor );
else
g.setColor( backColor );
int offset = drawFrame ? 1 : 0;
g.setClip( offset, offset, getSize().width - 1 - offset, getSize().height );
g.fillRect( offset, currentY - fontMetrics.getAscent() - colPadding, getSize().width,
fontHeight + ( colPadding * 2 ) );
}
/**
* Get the parent element from the XModel. Apply the header style increment
* the currentY and draw the first row of data in the XModel. Apply the general
* style, loop thru the remaining elements in the XModel and render them.
* @param g The graphics object
*/
private void render( Graphics g )
{
rendered = true;
if ( model != null ) {
itemIdx = 0;
int startClipY = ( int )g.getClipBounds().y;
int endClipY = ( int )( g.getClipBounds().height + g.getClipBounds().y );
// Render the header
startRow = renderHeader( g, model, startClipY );
headerHeight = currentY + ( colPadding * 2 ) + 1;
// Render the content
int numChildren = model.getNumChildren();
applyStyle( g, tableStyle );
if ( borderColor == null )
borderColor = getForeground().darker();
int currentRow = getCurrentRow();
for ( int rowIdx = startRow; rowIdx < numChildren; rowIdx++ ) {
if ( interactiveTable && ( rowIdx == currentRow ) )
applySelectedStyle( g, selectedStyle );
currentY += fontHeight + ( colPadding * 2 );
XModel rowModel = model.get( rowIdx );
if ( rowModel != null ){
if ( ( ( String )rowModel.get( 0 ).get() ).length() > 0 )
itemIdx++;
if ( currentY > ( startClipY - ( fontHeight + ( colPadding * 2 ) ) ) )
renderRow( itemIdx, g, rowModel, false );
// Reapply the normal style
if ( interactiveTable && ( rowIdx == currentRow ) )
applyStyle( g, tableStyle );
endClipY = ( int )( g.getClipBounds().height + g.getClipBounds().y );
if ( currentY > endClipY )
break;
}
}
if (( currentRow >= 0 ) && ( currentRow < numChildren ))
model.get( currentRow );
}
}
/**
* Renders the table header
* @param g The graphics object
* @param model The data model
*/
private int renderHeader( Graphics g, XModel model, int startClipY )
{
applyStyle( g, headerStyle );
if ( model.getNumChildren() > 0 ) {
XModel rowModel = model.get( 0 );
String tag = rowModel.getTagName();
if ( tag.equalsIgnoreCase( "th" ) ) {
// A <th>...</th> header record
currentY += fontMetrics.getAscent() + colPadding;
if ( startClipY < rowHeight )
renderRow( 0, g, rowModel, true );
return 1;
}
else if ( tag.equalsIgnoreCase( "tr" ) ) {
// No header specified in the XML
return 0;
}
else {
// An extended model node type e.g. XLib::XTableModelNode
currentY += fontMetrics.getAscent() + colPadding;
if ( startClipY >= rowHeight )
return 0;
int currentX = 0;
clearRow( g, 0 );
for ( int fieldIdx = 0; fieldIdx < model.getNumAttributes(); fieldIdx++ )
currentX = renderCell( g, currentX, model, model.getAttribName( fieldIdx ), fieldIdx, true );
// Draw a line under the row
g.drawLine( 1, currentY + ( colPadding * 2 ) + 1, getSize().width,
currentY + ( colPadding * 2 ) + 1 );
}
}
return 0;
}
/**
* Initialise the currentY coordinate and call the render function with the
* Graphics object. When finished draw a line around the XTable.
* @param g the graphics context
*/
public void paint( Graphics g )
{
update( g );
}
/**
* Applies a named style to the Graphics context.
* @param styleName
* @param g
*/
private void applyStyle( Graphics g, String styleName )
{
if ( styleName == null )
styleName = "base";
XStyle style = XProjectManager.getStyleManager().getStyle( styleName );
foreColor = style.getStyleAsColor( XStyle.COLOR_FORE );
if ( foreColor == null )
foreColor = getForeground();
backColor = style.getStyleAsColor( XStyle.COLOR_BACK );
if ( backColor == null )
backColor = getBackground();
if ( backColor != null ) {
darkerColor = new Color(
( int ) ( backColor.getRed() * darkerFactor ),
( int ) ( backColor.getGreen() * darkerFactor ),
( int ) ( backColor.getBlue() * darkerFactor ) );
}
font = XProjectManager.getStyleManager().getFont( style );
g.setFont( font );
fontMetrics = g.getFontMetrics();
fontHeight = fontMetrics.getHeight();
rowHeight = ( fontHeight + ( colPadding * 2 ) );
}
/**
* Applies a named style to the Graphics context.
* @param styleName
* @param g
*/
private void applySelectedStyle( Graphics g, String styleName )
{
if ( styleName == null )
styleName = "base";
XStyle style = XProjectManager.getStyleManager().getStyle( styleName );
foreColor = style.getStyleAsColor( XStyle.COLOR_FORE );
if ( foreColor == null )
foreColor = getForeground();
backColor = style.getStyleAsColor( XStyle.COLOR_BACK );
if ( backColor == null )
backColor = getBackground();
darkerColor = backColor;
}
/**
* Sets the indexof the selected row
* @param idx the new selected row
*/
public void setSelectedRow( int idx )
{
selectedRow = Math.max( 0, Math.min( idx, model.getNumChildren()-1 ));
syncModel();
repaint();
}
/**
* Get the index of the selected row
* @return the index of the selected row
*/
public int getSelectedRow()
{
return selectedRow;
}
/**
* Tie the model selection to this table's selection
* @param doUpdate true to tie the selections together, false to ignore
*/
public void setUpdateModelSelection( boolean doUpdate )
{
updateModelSelection = doUpdate;
syncModel();
}
/**
* Update the underlying model's selection index with the tables index.
*/
private void syncModel()
{
if ( updateModelSelection )
model.get( startRow + selectedRow );
}
/**
* Handles the mouse click by changeing the selected row.
* @param e the mouse event
*/
public void mouseClicked( MouseEvent e )
{
}
/**
* Mouse event handler for mouse enter event
* @param e the mouse event
*/
public void mouseEntered( MouseEvent e )
{
}
/**
* Mouse event handler for mouse exit event
* @param e the mouse event
*/
public void mouseExited( MouseEvent e )
{
}
/**
* Mouse event handler for mouse pressed event
* @param e the mouse event
*/
public void mousePressed( MouseEvent e )
{
}
/**
* Mouse event handler for mouse released event
* @param e the mouse event
*/
public void mouseReleased( MouseEvent e )
{
removeCurrentComponent();
rowHeight = ( fontHeight + ( colPadding * 2 ) );
int oldSelection = selectedRow;
int y = e.getY();
int x = e.getX();
y -= headerHeight;
if ( model != null ) {
int maxRow = model.getNumChildren() - 1;
selectedRow = Math.min( y / rowHeight, maxRow );
int maxCol = components.length;
selectedColumn = 0;
int xMouse = x;
for ( int i = 0; i < maxCol; i++ ) {
xMouse -= colWidth[ i ];
if ( xMouse > 0 )
selectedColumn++;
else
break;
}
if ( selectedColumn < components.length ) {
if ( components[ selectedColumn ] != null ) {
setCellComponent( selectedColumn, selectedRow );
}
}
repaint( 0, headerHeight + rowHeight * oldSelection, 1000, rowHeight );
repaint( 0, headerHeight + rowHeight * selectedRow, 1000, rowHeight );
if ( itemListener != null )
itemListener.itemStateChanged( new ItemEvent( owner, ItemEvent.ITEM_STATE_CHANGED, new Integer( selectedRow ),
ItemEvent.SELECTED ) );
}
syncModel();
}
/**
* Remove the current component
*/
public void removeCurrentComponent()
{
if ( currentComponent != null )
owner.getComponentPanel().remove( currentComponent );
}
/**
* Check to see if there is a value in the components array at position 'col'
* If so create an instance of the component and add it to the componentPanel
* Create a binding to the model and set it to the local variable 'editBinding'
* @param col The column of the model which was selected
* @param row The row of the model which was selected
*/
private void setCellComponent( int col, int row )
{
try {
int x = 0;
for ( int i = 0; i < col; i++ )
x += colWidth[ i ];
int width = colWidth[ col ] - colPadding;
int y = rowHeight + ( rowHeight * row );
int height = rowHeight;
if ( ( row + 1 ) < model.getNumChildren() ) {
Component c = (Component)Class.forName( components[ col ].toString().trim() ).newInstance();
c.addKeyListener( this );
c.setBounds( x, y, width, height );
Container panel = owner.getComponentPanel();
panel.add( c, 0 );
currentComponent = c;
XModel rowModel = ( XModel )model.get( row + 1 );
XModel bindModel = ( XModel )rowModel.get( col );
Hashtable bindingConfig = new Hashtable();
Hashtable instanceConfig = new Hashtable();
XTextBinding binding = new XTextBinding();
binding.setup( XProjectManager.getCurrentProject(), c, bindingConfig, instanceConfig );
binding.setSource( bindModel );
binding.setOutput( bindModel, null );
binding.get();
editBinding = binding;
}
}
catch ( Exception ex ) {
ex.printStackTrace();
}
}
/**
* Key event handler or the key press event
* @param e the event
*/
public void keyPressed( KeyEvent e )
{
if ( !e.getSource().equals( currentComponent ) ) {
if ( model != null ) {
int oldSelection = selectedRow;
int numChildren = model.getNumChildren();
int keyCode = e.getKeyCode();
ScrollPane sp = ( ( ScrollPane )getParent().getParent() );
int y = ( int )sp.getScrollPosition().y;
if ( ( keyCode == e.VK_UP ) || ( keyCode == VK_KP_UP ) ) {
selectedRow = Math.max( 0, selectedRow - 1 );
sp.setScrollPosition( 0, y - rowHeight );
}
else if ( ( keyCode == e.VK_DOWN ) || ( keyCode == VK_KP_DOWN ) ) {
selectedRow = Math.min( numChildren - 1, selectedRow + 1 );
sp.setScrollPosition( 0,
Math.min( y + rowHeight, ( ( numChildren - 2 ) * rowHeight ) ) );
}
else
return;
repaint( 0, headerHeight + rowHeight * oldSelection, 1000, rowHeight );
repaint( 0, headerHeight + rowHeight * selectedRow, 1000, rowHeight );
if ( itemListener != null )
itemListener.itemStateChanged( new ItemEvent( owner,
ItemEvent.ITEM_STATE_CHANGED, new Integer( selectedRow ),
ItemEvent.SELECTED ) );
}
}
syncModel();
}
/**
* Key event handler or the key typed event
* @param e the event
*/
public void keyTyped( KeyEvent e )
{
}
/**
* Key event handler or the key released event
* @param e the event
*/
public void keyReleased( KeyEvent e )
{
if ( e.getSource().equals( currentComponent ) )
editBinding.set();
}
/**
* Update the display
* @param g the graphics context
*/
public void update( Graphics g )
{
if ( drawFrame )
g.drawRect( 0, 0, getSize().width - 1, getSize().height - 1 );
currentY = 0;
render( g );
// g.drawRect( 0, 0, getSize().width - 1, getSize().height - 1 );
}
/**
* Calculate the size of the content. This method is called from within the
* paint method and recalculates the required size for display of the content.
* If a scrollpane is the parent then this control is resized so that all the
* content will be visible. The scrollpane may initially have no scrollbar so
* to avoid flicker and multiple repaints as the control is sized and offscreen
* graphics context is used for the sizing.
* @return The table size
*/
public Dimension calcSize()
{
Dimension d = new Dimension();
if (( model != null ) && ( model.getNumChildren() > 0 )) {
// This width should be calculated based on the content of the cells.
XModel rowModel = model.get( 0 );
int numCols = rowModel.getNumChildren();
for ( int i = 0; i < numCols; i++ )
d.width += colWidth[ i ];
Graphics g = getGraphics();
if ( g != null ) {
if ( tableStyle != null ) {
// This doesn't properly account for different font sizes in the header but
// a little more or less height probably won't matter much.
XStyle style = XProjectManager.getStyleManager().getStyle( tableStyle );
font = XProjectManager.getStyleManager().getFont( style );
}
g.setFont( font );
fontMetrics = g.getFontMetrics();
fontHeight = fontMetrics.getHeight();
g.dispose();
}
d.height = ( ( fontHeight + ( colPadding * 2 ) + 1 ) ) * ( model.getNumChildren() + 1 ) + 10;
}
return d;
}
int getNumRows()
{
if ( model != null )
return model.getNumChildren();
return 0;
}
/**
* Set the table column width.
* @param fieldIdx the field index
* @param w the new column width
*/
public void setColWidth( int fieldIdx, int w )
{
colWidth[ fieldIdx ] = w;
Dimension d = calcSize();
setBounds( 0, 0, ( int )d.getSize().width, ( int )d.getSize().height );
owner.getComponentPanel().setBounds( 0, 0, ( int )d.getSize().width, ( int )d.getSize().height );
}
/**
* Gets the index in the model of the currently selected row.
* @return the row offset
*/
public int getCurrentRow()
{
return startRow + selectedRow;
}
/**
* Gets the offset in the model of the first row of data. This takes account
* of how the table header is stored in the model. In the static data or XML
* representations the header is recorded with a row of <TH> elements, whereas
* when the data has originated in a database then a custom node type may be
* used instead.
* @return the row offset
*/
public int getFirstRow()
{
return startRow;
}
/**
* Adds the specified item listener to receive item events from
* this list. Item events are sent in response to user input, but not
* in response to calls to <code>select</code> or <code>deselect</code>.
* If listener <code>l</code> is <code>null</code>,
* no exception is thrown and no action is performed.
*
* @param l the item listener
* @see #removeItemListener( ItemListener )
* @see java.awt.event.ItemEvent
* @see java.awt.event.ItemListener
* @since JDK1.1
*/
public synchronized void addItemListener(ItemListener l) {
if ( l == null ) {
return;
}
itemListener = AWTEventMulticaster.add( itemListener, l );
}
/**
* Removes the specified item listener so that it no longer
* receives item events from this list.
* If listener <code>l</code> is <code>null</code>,
* no exception is thrown and no action is performed.
*
* @param l the item listener
* @see #addItemListener
* @see java.awt.event.ItemEvent
* @see java.awt.event.ItemListener
* @since JDK1.1
*/
public synchronized void removeItemListener( ItemListener l )
{
if ( l == null ) {
return;
}
itemListener = AWTEventMulticaster.remove( itemListener, l );
}
/**
* Returns the selected items on the list in an array of objects.
* @see ItemSelectable
* @return the selected object/row
*/
public Object[] getSelectedObjects()
{
Integer sel[] = new Integer[ 1 ];
sel[ 0 ] = new Integer( selectedRow );
return sel;
}
/**
* Check if the component has rendered yet.
* @return true if rendered at least once
*/
boolean hasRendered()
{
return rendered;
}
transient ItemListener itemListener;
}