/* * [ColorTokenPanel.java] * * Summary: Renders a string of tokens, usually representing Java source code. Does not handle its own scrolling. * * Copyright: (c) 2004-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 4.0 2009-04-12 shorter style names, improved highlighting. */ 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.*; /** * 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 */ class ColorTokenPanel 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. */ // private Dimension dimension = new Dimension( 10, 10 ); /** * 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 ColorTokenPanel() { // get swing to clear the background for us, tell it we don't paint all the pixels. this.setOpaque( true ); } /** * 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; // pick a baseline just prior to the band for safety. int firstBaseline = topOfBand - Geometry.LEADING_PX; // home in on a unique 0-based band int band = Arrays.binarySearch( baselines, firstBaseline ); if ( band < 0 ) { // convert insertion point to the band below. int insert = -band - 1; band = insert - 1; band = Math.min( Math.max( 0, band ), bandCount - 1 ); } // As side benefit, we get the startAtBaseline and starAtLineNumber startAtBaseline = baselines[ band ]; // startAtLineNumber is 1-based. 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 */ // pick a baseline just after the band for safety. // With multiple nls it could be part way through the band, but with // nothing // to render after it. int lastBaseline = bottomOfBand + Geometry.LEADING_PX; // home in on a unique 0-based band int band = Arrays.binarySearch( baselines, lastBaseline ); if ( band < 0 ) { // with an inexact hit we want the conservative choice the band // after the insert point. band = Math.min( Math.max( 0, -band - 1 ), bandCount - 1 ); } if ( band == bandCount - 1 ) { // we are on the last band, render even the very last token. return tokens.length - 1; } else { /* * the last token of the band is the token just before the token on * the start of the next band. */ 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; } // these are a little bigger than we need. bandCount = 0; baselines = new int[ totalLines ]; firstTokensInBand = new int[ totalLines ]; firstLineNumbersInBand = new int[ totalLines ]; // debugging, fill with easy to spot invalid values. 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() { // trim arrays back precisely to size so that binary search will work. 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 ) { // We avoid rendering before or after the clip region. // Normally we only render a 4 pixel high band at a time. Rectangle r = g.getClipBounds(); // No need to clear the background of just the clip region // because setOpaque handles it // g.setColor( this.getBackground() ); // g.fillRect ( r.x, r.y, r.width, r.height ); // render all the tokens, some may be offscreen, but no matter. if ( tokens == null || tokens.length == 0 ) { return; } // use rendering hints // locals // true if this is the first token on theline boolean firstTokenOnLine; // index of first token to render // index of last token to render. int firstTokenToRender; int lastTokenToRender; // x left, for token rendering int x; // y basesline for token rendering int y; // line number rendering 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 ) { // capture base line info of this line for the accelerator. lineRenderedAt( y, lineNumber, i ); } if ( t instanceof NL ) { // render blank lines compressed. int lines = ( ( NL ) t ).getCount(); switch ( lines ) { case 1: // single space y += Geometry.LEADING_PX; break; case 2: // 1.5 spacing y += Geometry.LEADING_PX + Geometry.BLANK_LINE_HEIGHT_PX; break; case 3: default: // anything bigger, just double space. y += Geometry.LEADING_PX + ( Geometry.BLANK_LINE_HEIGHT_PX * 2 ); break; } lineNumber += lines;// leave off line numbers, to avoid // scrunching x = Geometry.LEFT_PADDING_PX; firstTokenOnLine = true; } else { // text-rendering or space token if ( hasLineNumbers && firstTokenOnLine ) { // draw the line number g.setColor( Token.getLineNumberForeground() ); g.setFont( Token.getLineNumberFont() ); String digits = Integer.toString( lineNumber ); // right justify int width = g.getFontMetrics().stringWidth( digits ); // x,y is bottom left corner of text 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; } // end else } // end for // we now have captured all token baselines, even if bandCount was tiny. if ( !accelerated ) { prepareAccelerator2(); accelerated = true; } } // end render /** * called whenever system has a slice to render * * @param g Graphics defining where and region to paint. */ public void paintComponent( Graphics g ) { // paintComponent will clear the region to the background colour super.paintComponent( g ); // don't meddle with original Graphics2D g2d = ( Graphics2D ) g; g2d.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON ); g2d.setRenderingHint( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY ); // if wanted to smooth geometric shapes too // g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, // RenderingHints.VALUE_ANTIALIAS_ON ); 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( "ColorTokenPanel.set width:" + width + " height:" + height + " hasLineNumbers:" + hasLineNumbers + " lineNumberWidth:" + lineNumberWidth ); } Dimension dimension = new Dimension( width, height ); // only available in 1.5+ this.setMinimumSize( dimension ); this.setPreferredSize( dimension ); this.setMaximumSize( dimension ); this.setSize( dimension ); this.hasLineNumbers = hasLineNumbers; this.lineNumberWidth = lineNumberWidth; // adding or removing line numbers means new size. // caller must setPreferred size 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; } }