/* * [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