package com.mindprod.jdisplay;
import com.mindprod.jtokens.NL;
import com.mindprod.jtokens.Token;
import javax.swing.JPanel;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.util.Arrays;
import static java.lang.System.out;
/**
* Renders a string of tokens, usually representing Java source code. Does not handle its own scrolling.
*
* @author Roedy Green, Canadian Mind Products
* @version 4.0 2009-04-12 shorter style names, improved highlighting.
* @since 2004
*/
public class PrettyCanvas extends JPanel
{
/**
* true if want extra debug output
*/
private static final boolean DEBUGGING = false;
/**
* Dimensions of the scrollable footprint. Start with dummy in case we get queried before set called.
*/
/**
* Array of tokens to render
*/
private Token[] tokens;
/**
* has the accelerator to render only necessary tokens kicked in yet?
*/
private boolean accelerated = false;
/**
* true if want lineNumbers
*/
private boolean hasLineNumbers;
/**
* baseline in pixels down from top of canvas that bandCount renders on. indexed by bandCount. There will be one
* entry per non-blank line here.
*/
private int[] baselines;
/**
* first line number to render in a given band.
*/
private int[] firstLineNumbersInBand;
/**
* first token to render in a given band.
*/
private int[] firstTokensInBand;
/**
* counts how many bands we have. If there were no blank lines, would be same as number of lines. Normally the value
* is a little less that the number of lines since a strip of vertical white space counts as one bandCount.
*/
private int bandCount;
/**
* how many pixels wide line numbers are
*/
private int lineNumberWidth;
/**
* top most baseline where we start rendering a bandCount.
*/
private int startAtBaseline;
/**
* 1-based line number to start rendering the current bandCount.
*/
private int startAtLineNumber;
/**
* Total lines of text in the entire array of Tokens, which is considerably smaller than the total number of
* tokens.
*/
private int totalLines;
/**
* Constructor
*/
public PrettyCanvas()
{
this.setOpaque( true );
}
/**
* called whenever system has a slice to render
*
* @param g Graphics defining where and region to paint.
*/
public void paintComponent( Graphics g )
{
super.paintComponent( g );
Graphics2D g2d = ( Graphics2D ) g;
g2d.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON );
g2d.setRenderingHint( RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY );
render( g2d );
}
/**
* @param width in pixels of the scrollable region including room for line numbers and margins, but not
* scrollbars.
* @param height in pixel of the sscrollableregion including room for margins, but not scrollbars.
* @param hasLineNumbers true if want line numbers applied down the left hand side.
* @param lineNumberWidth with of the line number column
*/
public void set( int width,
int height,
boolean hasLineNumbers,
int lineNumberWidth )
{
if ( DEBUGGING )
{
out.println( "PrettyCanvas.set width:" + width
+ " height:" + height
+ " hasLineNumbers:" + hasLineNumbers
+ " lineNumberWidth:" + lineNumberWidth );
}
Dimension dimension = new Dimension( width, height );
this.setMinimumSize( dimension );
this.setPreferredSize( dimension );
this.setMaximumSize( dimension );
this.setSize( dimension );
this.hasLineNumbers = hasLineNumbers;
this.lineNumberWidth = lineNumberWidth;
accelerated = false;
this.invalidate();
}
/**
* Set tokens to display
*
* @param tokens array of tokens, without lead or trailing NL()
* @param totalLines number of lines of text to render.
*/
public void setTokens( Token[] tokens, int totalLines )
{
this.tokens = tokens;
this.totalLines = totalLines;
this.accelerated = false;
}
/**
* accelerate rendering by computing just which tokens need to be rendered for a given bandCount. Get the index of
* the first Token
*
* @param r clip region to be rendered.
*
* @return first token index that needs to be rendered.
*/
private int firstTokenNeedToRender( Rectangle r )
{
int topOfBand = r.y;
int firstBaseline = topOfBand - Geometry.LEADING_PX;
int band = Arrays.binarySearch( baselines, firstBaseline );
if ( band < 0 )
{
int insert = -band - 1;
band = insert - 1;
band = Math.min( Math.max( 0, band ), bandCount - 1 );
}
startAtBaseline = baselines[ band ];
startAtLineNumber = firstLineNumbersInBand[ band ];
return firstTokensInBand[ band ];
}
/**
* accelerate rendering by computing just which tokens need to be rendered for a given bandCount.
*
* @param r clip region to be rendered.
*
* @return last token index that needs to be rendered.
*/
@SuppressWarnings( { "UnusedAssignment" } )
private int lastTokenNeedToRender( Rectangle r )
{
int bottomOfBand = r.y + r.height;/* y increases down the screen */
int lastBaseline = bottomOfBand + Geometry.LEADING_PX;
int band = Arrays.binarySearch( baselines, lastBaseline );
if ( band < 0 )
{
band = Math.min( Math.max( 0, -band - 1 ), bandCount - 1 );
}
if ( band == bandCount - 1 )
{
return tokens.length - 1;
}
else
{
return firstTokensInBand[ band + 1 ] - 1;
}
}
/**
* Record where on page we started rendering a given band i.e. line with text on it.
*
* @param baseline y in of baseline in pixels from the top of canvas.
* @param lineNumber one-based line number being rendered
* @param tokenIndex index of first token on the line, including possibly NL though normally it would be the last
* token of the previous line.
*/
private void lineRenderedAt( int baseline, int lineNumber, int tokenIndex )
{
baselines[ bandCount ] = baseline;
firstTokensInBand[ bandCount ] = tokenIndex;
firstLineNumbersInBand[ bandCount ] = lineNumber;
bandCount++;
}
/**
* Clear binary search arrays used to accelerate rendering by finding only those tokens we need to render.
*/
private void prepareAccelerator1()
{
if ( tokens == null || tokens.length == 0 )
{
return;
}
bandCount = 0;
baselines = new int[ totalLines ];
firstTokensInBand = new int[ totalLines ];
firstLineNumbersInBand = new int[ totalLines ];
for ( int i = 0; i < totalLines; i++ )
{
baselines[ i ] = -10;
firstTokensInBand[ i ] = -20;
firstLineNumbersInBand[ i ] = -30;
}
}
/**
* Prepare to use the accelerator by trimming its arrays back to perfect size. We have collected data on where each
* band is rendering.
*/
private void prepareAccelerator2()
{
int[] old = baselines;
baselines = new int[ bandCount ];
System.arraycopy( old, 0, baselines, 0, bandCount );
old = firstTokensInBand;
firstTokensInBand = new int[ bandCount ];
System.arraycopy( old, 0, firstTokensInBand, 0, bandCount );
old = firstLineNumbersInBand;
firstLineNumbersInBand = new int[ bandCount ];
System.arraycopy( old, 0, firstLineNumbersInBand, 0, bandCount );
}
/**
* does drawing. similar to logic in Footprint.s2CalcPayloadFootprint
*
* @param g where to paint
*/
@SuppressWarnings( { "PointlessArithmeticExpression" } )
private void render( Graphics2D g )
{
Rectangle r = g.getClipBounds();
if ( tokens == null || tokens.length == 0 )
{
return;
}
boolean firstTokenOnLine;
int firstTokenToRender;
int lastTokenToRender;
int x;
int y;
int lineNumber;
if ( accelerated )
{
firstTokenToRender = firstTokenNeedToRender( r );
lastTokenToRender = lastTokenNeedToRender( r );
x = Geometry.LEFT_PADDING_PX;
y = startAtBaseline;
lineNumber = startAtLineNumber;
firstTokenOnLine = true;
}
else
{
prepareAccelerator1();
firstTokenToRender = 0;
lastTokenToRender = tokens.length - 1;
x = Geometry.LEFT_PADDING_PX;
y = Geometry.TOP_PADDING_PX + Geometry.LEADING_PX;
lineNumber = 1;
firstTokenOnLine = true;
}
if ( DEBUGGING )
{
out.println( "firstToken:"
+ firstTokenToRender
+ " lastToken:"
+ lastTokenToRender
+ " x:"
+ x
+ " ybaseline:"
+ y
+ " r.y:"
+ r.y
+ " r.height:"
+ r.height
+ " ln:"
+ lineNumber );
}
for ( int i = firstTokenToRender; i <= lastTokenToRender; i++ )
{
Token t = tokens[ i ];
if ( !accelerated && firstTokenOnLine )
{
lineRenderedAt( y, lineNumber, i );
}
if ( t instanceof NL )
{
int lines = ( ( NL ) t ).getCount();
switch ( lines )
{
case 1:
y += Geometry.LEADING_PX;
break;
case 2:
y += Geometry.LEADING_PX + Geometry.BLANK_LINE_HEIGHT_PX;
break;
case 3:
default:
y += Geometry.LEADING_PX + ( Geometry.BLANK_LINE_HEIGHT_PX * 2 );
break;
}
lineNumber += lines;// leave off line numbers, to avoid
x = Geometry.LEFT_PADDING_PX;
firstTokenOnLine = true;
}
else
{
if ( hasLineNumbers && firstTokenOnLine )
{
g.setColor( Token.getLineNumberForeground() );
g.setFont( Token.getLineNumberFont() );
String digits = Integer.toString( lineNumber );
int width = g.getFontMetrics().stringWidth( digits );
g.drawString( digits, x + lineNumberWidth - width, y );
x += lineNumberWidth + Geometry
.LINE_NUMBER_PADDING_PX;
}
g.setColor( t.getForeground() );
final Font font = t.getFont();
g.setFont( font );
final String text = t.getText();
g.drawString( text, x, y );
x += g.getFontMetrics().stringWidth( text );
firstTokenOnLine = false;
}
}
if ( !accelerated )
{
prepareAccelerator2();
accelerated = true;
}
}
}