/*
* soapUI, copyright (C) 2004-2011 eviware.com
*
* soapUI is free software; you can redistribute it and/or modify it under the
* terms of version 2.1 of the GNU Lesser General Public License as published by
* the Free Software Foundation.
*
* soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details at gnu.org.
*/
package org.syntax.jedit;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import javax.swing.JComponent;
import javax.swing.ToolTipManager;
import javax.swing.text.PlainDocument;
import javax.swing.text.Segment;
import javax.swing.text.TabExpander;
import javax.swing.text.Utilities;
import org.syntax.jedit.tokenmarker.Token;
import org.syntax.jedit.tokenmarker.TokenMarker;
import com.eviware.soapui.SoapUI;
/**
* The text area repaint manager. It performs double buffering and paints lines
* of text.
*
* @author Slava Pestov
* @version $Id$
*/
public class TextAreaPainter extends JComponent implements TabExpander
{
private int areaWidth;
private int areaHeight;
/**
* Creates a new repaint manager. This should be not be called directly.
*/
public TextAreaPainter( JEditTextArea textArea, TextAreaDefaults defaults )
{
this.textArea = textArea;
setAutoscrolls( true );
setDoubleBuffered( true );
setOpaque( true );
setBorder( null );
ToolTipManager.sharedInstance().registerComponent( this );
currentLine = new Segment();
currentLineIndex = -1;
setCursor( Cursor.getPredefinedCursor( Cursor.TEXT_CURSOR ) );
setFont( new Font( "Monospaced", Font.PLAIN, 14 ) );
setForeground( Color.black );
setBackground( Color.white );
blockCaret = defaults.blockCaret;
styles = defaults.styles;
// cols = defaults.cols;
// rows = defaults.rows;
caretColor = defaults.caretColor;
selectionColor = defaults.selectionColor;
lineHighlightColor = defaults.lineHighlightColor;
lineHighlight = defaults.lineHighlight;
bracketHighlightColor = defaults.bracketHighlightColor;
bracketHighlight = defaults.bracketHighlight;
paintInvalid = defaults.paintInvalid;
eolMarkerColor = defaults.eolMarkerColor;
eolMarkers = defaults.eolMarkers;
}
/**
* Returns if this component can be traversed by pressing the Tab key. This
* returns false.
*/
public final boolean isManagingFocus()
{
return false;
}
/**
* Returns the syntax styles used to paint colorized text. Entry <i>n</i>
* will be used to paint tokens with id = <i>n</i>.
*
* @see org.syntax.jedit.Token
*/
public final SyntaxStyle[] getStyles()
{
return styles;
}
/**
* Sets the syntax styles used to paint colorized text. Entry <i>n</i> will
* be used to paint tokens with id = <i>n</i>.
*
* @param styles
* The syntax styles
* @see org.syntax.jedit.Token
*/
public final void setStyles( SyntaxStyle[] styles )
{
this.styles = styles;
repaint();
}
/**
* Returns the caret color.
*/
public final Color getCaretColor()
{
return caretColor;
}
/**
* Sets the caret color.
*
* @param caretColor
* The caret color
*/
public final void setCaretColor( Color caretColor )
{
this.caretColor = caretColor;
invalidateSelectedLines();
}
/**
* Returns the selection color.
*/
public final Color getSelectionColor()
{
return selectionColor;
}
/**
* Sets the selection color.
*
* @param selectionColor
* The selection color
*/
public final void setSelectionColor( Color selectionColor )
{
this.selectionColor = selectionColor;
invalidateSelectedLines();
}
/**
* Returns the line highlight color.
*/
public final Color getLineHighlightColor()
{
return lineHighlightColor;
}
/**
* Sets the line highlight color.
*
* @param lineHighlightColor
* The line highlight color
*/
public final void setLineHighlightColor( Color lineHighlightColor )
{
this.lineHighlightColor = lineHighlightColor;
invalidateSelectedLines();
}
/**
* Returns true if line highlight is enabled, false otherwise.
*/
public final boolean isLineHighlightEnabled()
{
return lineHighlight;
}
/**
* Enables or disables current line highlighting.
*
* @param lineHighlight
* True if current line highlight should be enabled, false
* otherwise
*/
public final void setLineHighlightEnabled( boolean lineHighlight )
{
this.lineHighlight = lineHighlight;
invalidateSelectedLines();
}
/**
* Returns the bracket highlight color.
*/
public final Color getBracketHighlightColor()
{
return bracketHighlightColor;
}
/**
* Sets the bracket highlight color.
*
* @param bracketHighlightColor
* The bracket highlight color
*/
public final void setBracketHighlightColor( Color bracketHighlightColor )
{
this.bracketHighlightColor = bracketHighlightColor;
invalidateLine( textArea.getBracketLine() );
}
/**
* Returns true if bracket highlighting is enabled, false otherwise. When
* bracket highlighting is enabled, the bracket matching the one before the
* caret (if any) is highlighted.
*/
public final boolean isBracketHighlightEnabled()
{
return bracketHighlight;
}
/**
* Enables or disables bracket highlighting. When bracket highlighting is
* enabled, the bracket matching the one before the caret (if any) is
* highlighted.
*
* @param bracketHighlight
* True if bracket highlighting should be enabled, false otherwise
*/
public final void setBracketHighlightEnabled( boolean bracketHighlight )
{
this.bracketHighlight = bracketHighlight;
invalidateLine( textArea.getBracketLine() );
}
/**
* Returns true if the caret should be drawn as a block, false otherwise.
*/
public final boolean isBlockCaretEnabled()
{
return blockCaret;
}
/**
* Sets if the caret should be drawn as a block, false otherwise.
*
* @param blockCaret
* True if the caret should be drawn as a block, false otherwise.
*/
public final void setBlockCaretEnabled( boolean blockCaret )
{
this.blockCaret = blockCaret;
invalidateSelectedLines();
}
/**
* Returns the EOL marker color.
*/
public final Color getEOLMarkerColor()
{
return eolMarkerColor;
}
/**
* Sets the EOL marker color.
*
* @param eolMarkerColor
* The EOL marker color
*/
public final void setEOLMarkerColor( Color eolMarkerColor )
{
this.eolMarkerColor = eolMarkerColor;
repaint();
}
/**
* Returns true if EOL markers are drawn, false otherwise.
*/
public final boolean getEOLMarkersPainted()
{
return eolMarkers;
}
/**
* Sets if EOL markers are to be drawn.
*
* @param eolMarkers
* True if EOL markers should be drawn, false otherwise
*/
public final void setEOLMarkersPainted( boolean eolMarkers )
{
this.eolMarkers = eolMarkers;
repaint();
}
/**
* Returns true if invalid lines are painted as red tildes (~), false
* otherwise.
*/
public boolean getInvalidLinesPainted()
{
return paintInvalid;
}
/**
* Sets if invalid lines are to be painted as red tildes.
*
* @param paintInvalid
* True if invalid lines should be drawn, false otherwise
*/
public void setInvalidLinesPainted( boolean paintInvalid )
{
this.paintInvalid = paintInvalid;
}
/**
* Adds a custom highlight painter.
*
* @param highlight
* The highlight
*/
public void addCustomHighlight( Highlight highlight )
{
highlight.init( textArea, highlights );
highlights = highlight;
}
/**
* Highlight interface.
*/
public interface Highlight
{
/**
* Called after the highlight painter has been added.
*
* @param textArea
* The text area
* @param next
* The painter this one should delegate to
*/
void init( JEditTextArea textArea, Highlight next );
/**
* This should paint the highlight and delgate to the next highlight
* painter.
*
* @param gfx
* The graphics context
* @param line
* The line number
* @param y
* The y co-ordinate of the line
*/
void paintHighlight( Graphics gfx, int line, int y );
/**
* Returns the tool tip to display at the specified location. If this
* highlighter doesn't know what to display, it should delegate to the
* next highlight painter.
*
* @param evt
* The mouse event
*/
String getToolTipText( MouseEvent evt );
}
/**
* Returns the tool tip to display at the specified location.
*
* @param evt
* The mouse event
*/
public String getToolTipText( MouseEvent evt )
{
if( highlights != null )
return highlights.getToolTipText( evt );
else
return null;
}
/**
* Returns the font metrics used by this component.
*/
public FontMetrics getFontMetrics()
{
return fm;
}
/**
* Sets the font for this component. This is overridden to update the cached
* font metrics and to recalculate which lines are visible.
*
* @param font
* The font
*/
public void setFont( Font font )
{
super.setFont( font );
fm = Toolkit.getDefaultToolkit().getFontMetrics( font );
textArea.recalculateVisibleLines();
}
/**
* Repaints the text.
*
* @param g
* The graphics context
*/
public void paint( Graphics gfx )
{
Rectangle clipRect = gfx.getClipBounds();
gfx.setColor( getBackground() );
gfx.fillRect( clipRect.x, clipRect.y, clipRect.width, clipRect.height );
// We don't use yToLine() here because that method doesn't
// return lines past the end of the document
int height = fm.getHeight();
int firstLine = textArea.getFirstLine();
int firstInvalid = firstLine + clipRect.y / height;
// Because the clipRect's height is usually an even multiple
// of the font height, we subtract 1 from it, otherwise one
// too many lines will always be painted.
int lastInvalid = firstLine + ( clipRect.y + clipRect.height - 1 ) / height;
try
{
TokenMarker tokenMarker = textArea.getDocument().getTokenMarker();
int x = 0; // -textArea.getHorizontalOffset();
for( int line = firstInvalid; line <= lastInvalid; line++ )
{
paintLine( gfx, tokenMarker, line, x );
}
if( tokenMarker != null && tokenMarker.isNextLineRequested() )
{
int h = clipRect.y + clipRect.height;
repaint( 0, h, getWidth(), getHeight() - h );
}
}
catch( Exception e )
{
System.err.println( "Error repainting line" + " range {" + firstInvalid + "," + lastInvalid + "}:" );
SoapUI.logError( e );
}
}
/**
* Marks a line as needing a repaint.
*
* @param line
* The line to invalidate
*/
public final void invalidateLine( int line )
{
repaint( 0, textArea.lineToY( line ) + fm.getMaxDescent() + fm.getLeading(), getWidth(), fm.getHeight() );
}
/**
* Marks a range of lines as needing a repaint.
*
* @param firstLine
* The first line to invalidate
* @param lastLine
* The last line to invalidate
*/
public final void invalidateLineRange( int firstLine, int lastLine )
{
repaint( 0, textArea.lineToY( firstLine ) + fm.getMaxDescent() + fm.getLeading(), getWidth(), ( lastLine
- firstLine + 1 )
* fm.getHeight() );
}
/**
* Repaints the lines containing the selection.
*/
public final void invalidateSelectedLines()
{
invalidateLineRange( textArea.getSelectionStartLine(), textArea.getSelectionEndLine() );
}
/**
* Implementation of TabExpander interface. Returns next tab stop after a
* specified point.
*
* @param x
* The x co-ordinate
* @param tabOffset
* Ignored
* @return The next tab stop after <i>x</i>
*/
public float nextTabStop( float x, int tabOffset )
{
if( tabSize == 0 )
initTabSize();
int offset = 0; // -textArea.getHorizontalOffset();
int ntabs = ( ( int )x - offset ) / tabSize;
return ( ntabs + 1 ) * tabSize + offset;
}
private void initTabSize()
{
tabSize = fm.charWidth( ' ' )
* ( ( Integer )textArea.getDocument().getProperty( PlainDocument.tabSizeAttribute ) ).intValue();
if( tabSize == 0 )
tabSize = fm.charWidth( ' ' ) * 4;
}
/**
* Returns the painter's preferred size.
*/
public Dimension getPreferredSize()
{
Dimension dim = new Dimension();
dim.width = fm.charWidth( 'w' ) * textArea.getMaxLineLength() + 5;
dim.height = fm.getHeight() * textArea.getLineCount() + 5;
return dim;
}
/**
* Returns the painter's minimum size.
*/
public Dimension getMinimumSize()
{
return getPreferredSize();
}
public Dimension getMaximumSize()
{
return getPreferredSize();
}
// package-private members
int currentLineIndex;
Token currentLineTokens;
Segment currentLine;
// protected members
protected JEditTextArea textArea;
protected SyntaxStyle[] styles;
protected Color caretColor;
protected Color selectionColor;
protected Color lineHighlightColor;
protected Color bracketHighlightColor;
protected Color eolMarkerColor;
protected boolean blockCaret;
protected boolean lineHighlight;
protected boolean bracketHighlight;
protected boolean paintInvalid;
protected boolean eolMarkers;
// protected int cols;
// protected int rows;
protected int tabSize;
protected FontMetrics fm;
protected Highlight highlights;
protected void paintLine( Graphics gfx, TokenMarker tokenMarker, int line, int x )
{
Font defaultFont = getFont();
Color defaultColor = getForeground();
currentLineIndex = line;
int y = textArea.lineToY( line );
int lineWidth = 0;
if( line < 0 || line >= textArea.getLineCount() )
{
if( paintInvalid )
{
paintHighlight( gfx, line, y );
styles[Token.INVALID].setGraphicsFlags( gfx, defaultFont );
gfx.drawString( "~", 0, y + fm.getHeight() );
}
}
else if( tokenMarker == null )
{
lineWidth = paintPlainLine( gfx, line, defaultFont, defaultColor, x, y );
}
else
{
lineWidth = paintSyntaxLine( gfx, tokenMarker, line, defaultFont, defaultColor, x, y );
}
if( lineWidth > areaWidth )
areaWidth = lineWidth;
}
protected int paintPlainLine( Graphics gfx, int line, Font defaultFont, Color defaultColor, int x, int y )
{
paintHighlight( gfx, line, y );
textArea.getLineText( line, currentLine );
gfx.setFont( defaultFont );
gfx.setColor( defaultColor );
y += fm.getHeight();
x = Utilities.drawTabbedText( currentLine, x, y, gfx, this, 0 );
if( eolMarkers )
{
gfx.setColor( eolMarkerColor );
gfx.drawString( ".", x, y );
}
return x;
}
protected int paintSyntaxLine( Graphics gfx, TokenMarker tokenMarker, int line, Font defaultFont,
Color defaultColor, int x, int y )
{
textArea.getLineText( currentLineIndex, currentLine );
currentLineTokens = tokenMarker.markTokens( currentLine, currentLineIndex );
paintHighlight( gfx, line, y );
gfx.setFont( defaultFont );
gfx.setColor( defaultColor );
y += fm.getHeight();
x = SyntaxUtilities.paintSyntaxLine( currentLine, currentLineTokens, styles, this, gfx, x, y );
if( eolMarkers )
{
gfx.setColor( eolMarkerColor );
gfx.drawString( ".", x, y );
}
return x;
}
protected void paintHighlight( Graphics gfx, int line, int y )
{
if( line >= textArea.getSelectionStartLine() && line <= textArea.getSelectionEndLine() )
paintLineHighlight( gfx, line, y );
if( highlights != null )
highlights.paintHighlight( gfx, line, y );
if( bracketHighlight && line == textArea.getBracketLine() )
paintBracketHighlight( gfx, line, y );
if( line == textArea.getCaretLine() )
paintCaret( gfx, line, y );
}
protected void paintLineHighlight( Graphics gfx, int line, int y )
{
int height = fm.getHeight();
y += fm.getLeading() + fm.getMaxDescent();
int selectionStart = textArea.getSelectionStart();
int selectionEnd = textArea.getSelectionEnd();
if( selectionStart == selectionEnd )
{
if( lineHighlight )
{
gfx.setColor( lineHighlightColor );
gfx.fillRect( 0, y, getWidth(), height );
}
}
else
{
gfx.setColor( selectionColor );
int selectionStartLine = textArea.getSelectionStartLine();
int selectionEndLine = textArea.getSelectionEndLine();
int lineStart = textArea.getLineStartOffset( line );
int x1, x2;
if( textArea.isSelectionRectangular() )
{
int lineLen = textArea.getTabExpandedLineLength( line );
x1 = textArea._offsetToX( line,
Math.min( lineLen, selectionStart - textArea.getLineStartOffset( selectionStartLine ) ) );
x2 = textArea._offsetToX( line,
Math.min( lineLen, selectionEnd - textArea.getLineStartOffset( selectionEndLine ) ) );
if( x1 == x2 )
x2++ ;
}
else if( selectionStartLine == selectionEndLine )
{
x1 = textArea._offsetToX( line, selectionStart - lineStart );
x2 = textArea._offsetToX( line, selectionEnd - lineStart );
}
else if( line == selectionStartLine )
{
x1 = textArea._offsetToX( line, selectionStart - lineStart );
x2 = getWidth();
}
else if( line == selectionEndLine )
{
x1 = 0;
x2 = textArea._offsetToX( line, selectionEnd - lineStart );
}
else
{
x1 = 0;
x2 = getWidth();
}
// "inlined" min/max()
gfx.fillRect( x1 > x2 ? x2 : x1, y, x1 > x2 ? ( x1 - x2 ) : ( x2 - x1 ), height );
}
}
protected void paintBracketHighlight( Graphics gfx, int line, int y )
{
int position = textArea.getBracketPosition();
if( position == -1 )
return;
y += fm.getLeading() + fm.getMaxDescent();
int x = textArea._offsetToX( line, position );
gfx.setColor( bracketHighlightColor );
// Hack!!! Since there is no fast way to get the character
// from the bracket matching routine, we use ( since all
// brackets probably have the same width anyway
gfx.drawRect( x, y, fm.charWidth( '(' ) - 1, fm.getHeight() - 1 );
}
protected void paintCaret( Graphics gfx, int line, int y )
{
if( textArea.isCaretVisible() )
{
int offset = textArea.getCaretPosition() - textArea.getLineStartOffset( line );
int caretX = textArea._offsetToX( line, offset );
int caretWidth = ( ( blockCaret || textArea.isOverwriteEnabled() ) ? fm.charWidth( 'w' ) : 1 );
y += fm.getLeading() + fm.getMaxDescent();
int height = fm.getHeight();
gfx.setColor( caretColor );
if( textArea.isOverwriteEnabled() )
{
gfx.fillRect( caretX, y + height - 1, caretWidth, 1 );
}
else
{
gfx.drawRect( caretX, y, caretWidth, height - 1 );
}
}
}
}