/*
* [Token.java]
*
* Summary: Describes one token to render.
*
* Copyright: (c) 2007-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:
* 1.3 2007-08-20 new colour scheme, separate style sheet
* add iframe rendering, use of snippet/ser and snippet/iframe
* 1.4 2008-02-13 fix getName to work whether class final or not.
* 1.5 2008-02-18 get JDisplay and CSS font renderings in closer sync
* 1.6 2008-02-23 fix getName to work whether class final or not.
* 1.7 2008-08-08 add parser for vanilla text
* 1.8 2009-01-01
* 1.9 2009-04-12 shorter style names, improved highlighting.
* 2.0 2009-04-19 tidy comments, more accurate colour names.
*/
package com.mindprod.jtokens;
import com.mindprod.common18.FontFactory;
import com.mindprod.common18.ST;
import com.mindprod.entities.EntifyStrings;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GraphicsEnvironment;
import java.awt.Panel;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import static com.mindprod.jtokens.TokenColourScheme.COMMON_FOREGROUND_FOR_LINE_NUMBER;
import static com.mindprod.jtokens.TokenFonts.LINE_NUMBER_FONT_SIZE_IN_POINTS;
import static com.mindprod.jtokens.TokenFonts.MONO_FONTS;
import static java.lang.System.*;
/**
* Describes one token to render.
*
* Roedy Green, Canadian Mind Products
*
* @author Roedy Green, Canadian Mind Products
* @version 2.0 2009-04-19 tidy comments, more accurate colour names.
* @since 2007-05-05
*/
public abstract class Token implements Cloneable, Serializable
{
static final boolean DEBUGGING = false;
/**
* version number for the class.
* Make match com.mindprod.jdisplay.Footprint.serialVersionUID.
*/
private static final long serialVersionUID = 29L;
/**
* best font to use for line numbers. May differ between preparer and end user.
*/
@SuppressWarnings( { "ConstantNamingConvention" } )
private static final Font lineNumberFont;
/**
* lookup font name to get font size adjustment factor
*/
private static final HashMap adjustFontSize = new HashMap<>( 25 );
/**
* Needed to get at FontMetrics
*/
private static final Panel dummyPanelForFontMetrics = new Panel();
/**
* HashSet of names of all supported font families, not Fonts themselves, just family names.
*/
private static HashSet allFontFamilies;
static
{
// factors to grow >1 or shrink <1 font sizes above nominal to compensate for mis-sized fonts.
adjustFontSize.put( "Arial", adjust( 112 ) );
adjustFontSize.put( "Bitstream Vera Sans", adjust( 117 ) );
adjustFontSize.put( "Bitstream Vera Sans Mono", adjust( 117 ) );
adjustFontSize.put( "Bitstream Vera Serif", adjust( 112 ) );
adjustFontSize.put( "Consolas", adjust( 103 ) );
adjustFontSize.put( "Constantia", adjust( 96 ) );
adjustFontSize.put( "Courier New", adjust( 93 ) );
adjustFontSize.put( "Dialog", adjust( 111 ) );
adjustFontSize.put( "DialogInput", adjust( 91 ) );
adjustFontSize.put( "Lucida Console", adjust( 113 ) );
adjustFontSize.put( "Lucida Sans Typewriter", adjust( 114 ) );
adjustFontSize.put( "Lucida Sans Unicode", adjust( 112 ) );
adjustFontSize.put( "Lucida Sans", adjust( 114 ) );
adjustFontSize.put( "Monospaced", adjust( 91 ) );
adjustFontSize.put( "SansSerif", adjust( 109 ) );
adjustFontSize.put( "Segoe UI", adjust( 108 ) );
adjustFontSize.put( "Tiresias PCfont Z", adjust( 114 ) );
// can't initialise until adjustFontSize map ready that bestFont uses.
lineNumberFont = bestFont( MONO_FONTS, Font.PLAIN, LINE_NUMBER_FONT_SIZE_IN_POINTS );
}
/**
* The text to render
*/
protected String text;
/**
* Constructor
*
* @param c single character of text for the token
*/
protected Token( char c )
{
this.text = String.valueOf( c );
}
/**
* Constructor
*
* @param text label text
*/
protected Token( String text )
{
this.text = text;
}
/**
* calculate a factor to increase/decrease nominal font size to make all fonts come out roughly the same as they
* do in CSS.
*
* @param eHeight height of an e in pixels at 200 points as measured with PixelRuler in FontShower
*
* @return factor
*/
private static float adjust( int eHeight )
{
return 1.085f * 112.0f / eHeight;
}
/**
* Get the most preferred supported font from a list
*
* @param preferredfontNames list of font names, best one first.
* @param fontStyle e.g. Font.PLAIN
* @param fontSize size of font in points.
*
* @return best font that is supported, or a Dialog font if none are.
*/
protected static Font bestFont( String[] preferredfontNames,
int fontStyle,
int fontSize )
{
if ( allFontFamilies == null )
{
// don't trust init to call findAllFontFamilies before calling us.
allFontFamilies = findAllFontFamilies();
}
// find first font of requests that is supported
for ( String preferredFontName : preferredfontNames )
{
// case-sensitive lookup
if ( allFontFamilies.contains( preferredFontName ) )
{
Float adjustment = adjustFontSize.get( preferredFontName );
if ( adjustment == null )
{
err.println( "Program bug. Unknown font. Cannot adjust true size. " + preferredFontName );
System.exit( 2 );
}
// adjust font size to normal size, rounded to nearest int.
fontSize = ( int ) ( ( fontSize * adjustment ) + .5f );
// we ask for the font with the correct capitalisation.
return FontFactory.build( preferredFontName, fontStyle, fontSize );
}
} // end for
return FontFactory.build( "Dialog", fontStyle, fontSize );
}
/**
* Find all font families supported on this machine. May be the machine preparing tokens or the one displaying
* them.
*
* @return HashSet of all supported families.
*/
private static HashSet findAllFontFamilies()
{
String[] names = ( GraphicsEnvironment.getLocalGraphicsEnvironment() )
.getAvailableFontFamilyNames();
// don't change case. These names are case-sensitive.
return new HashSet<>( Arrays.asList( names ) );
}
/**
* Get the font metrics for this font.
*
* @param font font we are planning on rendering in
* @param panel dummy component to define screen resolution
*
* @return FontMetrics object that knows character widths.
* @noinspection WeakerAccess
*/
static FontMetrics getFontMetrics( Font font, Component panel )
{
if ( font == null )
{
throw new IllegalArgumentException( "null Font" );
}
FontMetrics fm = panel.getFontMetrics( font );
if ( fm == null )
{
throw new IllegalArgumentException(
"no FontMetrics available for font " + font );
}
return fm;
}
/**
* get the text with special chars converted to entities.
*
* @return HTML with special characters converted to entities, but with out css span or similar sandwich yet
* applied.
*/
protected String getRawHTML()
{
return EntifyStrings.entifyHTML( getText() );
}
/**
* Font to render line numbers.
*
* @return Color object for rendering line numbers.
*/
public static Font getLineNumberFont()
{
return lineNumberFont;
}
/**
* Color to render line numbers.
*
* @return Font, in the correct size.
* @noinspection SameReturnValue
*/
public static Color getLineNumberForeground()
{
return COMMON_FOREGROUND_FOR_LINE_NUMBER;
}
/**
* Clone this object
*
* @return a copy of this Token. It will be the subtype but must be cast to that. Can't return Token pre JDK 1.5
*/
public Object clone() throws CloneNotSupportedException
{
try
{
return /* (Token) */super.clone();
}
catch ( CloneNotSupportedException e )
{
return null;
}
}
/**
* font to render this token.
*
* @return Font, in the correct size.
*/
public abstract Font getFont();
/**
* foreground colour to render this token.
*
* @return Color object.
*/
public abstract Color getForeground();
/**
* get the text surrounded by CSS html.
*
* @return decorated HTML in a span../span sandwich
*/
public String getHTML()
{
return getRawHTML();
}
/**
* get the name of this token type, i.e. the unqualified class name.
*
* @return classname of this token
* @noinspection WeakerAccess
*/
public String getName()
{
String className = this.getClass().toString().trim();
if ( className.startsWith( "final" ) )
{
// chop leading final
className = className.substring( "final".length() ).trim();
}
if ( className.startsWith( "class" ) )
{
className = className.substring( "class".length() ).trim();
}
if ( className.startsWith( "com.mindprod.jtokens." ) )
{
className = className.substring( "com.mindprod.jtokens.".length() ).trim();
}
return className;
}
/**
* gets the raw text to render this token, may contain spaces but not \n.
*
* @return The plain String representation of the text
*/
public String getText()
{
return text;
}
/**
* Sets the raw text to render this token , possibly containing.
*
* @param text data text as opposed to the HTML decorated version of the text to display for this Token.
*/
public void setText( String text )
{
this.text = text;
}
/**
* gets the raw text to render this token with lead/trail spaces trimmed..
*
* @return The plain String representation of the text
*/
public String getTrimmedText()
{
return getText().trim();
}
/**
* Can this token be collapsed with following space or with a following identical token?
*
* @return true if can be collapsed simply by combining text
*/
public boolean isCollapsible()
{
return true;
}
/**
* Can this token been collapse with the following given token
*
* @param t following token considered for merge
*
* @return true if the token in considered identical enough for merge.
*/
public boolean isCollapsible( Token t )
{
// Definables will have a more stringent condition
return isCollapsible() && this.getClass() == t.getClass();
}
/**
* Is this token useless? In other words would rendering it have no visible effect? Comments,
* NL and Space are NOT considered
* useless, unless they have 0 counts. Usually a token is useless because it has and empty or null text string.
*
* @return true if this token can be thrown away as useless
*/
public boolean isUseless()
{
return text == null || text.length() == 0;
}
/**
* debugging tool to display a Token
*
* @return String representation of the token.
*/
public String toString()
{
// also handles spaces.
final String text = this.getText();
final FontMetrics fm = Token.getFontMetrics( this.getFont(), dummyPanelForFontMetrics );
final int widthInPixels = fm.stringWidth( text );
// chop leading bubblegum off class name
return "Token: "
+ getName()
+ "\n"
+ " text: "
+ getText()
+ "\n"
+ " html: "
+ getHTML()
+ "\n"
+ " font: "
+ getFont().toString().substring( "java.awt.Font".length() )
+ "\n"
+ " color: "
+ ST.toString( getForeground() )
+ " width: "
+ widthInPixels
+ "\n";
}
} // end Token