/* * [JDisplay.java] * * Summary: Displays a serialised pre-parsed Java program in fancy fonts and colours. * * 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: * 1.2 2004-05-15 split off calcPreferredSize into its own * class * 1.3 2004-05-23 Put all logic about calculating panel and * frame size in PreferredSize None left to JDisplay macro. no bar * parameter. Computed automatically. manual control of when ScrollBars * used. * 1.4 2004-05-29 Flip back from Swing to AWT so that Ctrl-C * Ctrl-V will work. Even with AWT, I need a TextArea, not the * PrettyCanvas. Downside mainly was losing ability to turn or horizontal * and vertical scrollbars automatically. Adjust for fact scrollbars are * all or nothing. Can't have just vertical. Redo all tokenizers with * lookaheaad, and explicit handled boolean. Eliminate the enter method * on all tokenizers. Explicit list of all choices on default for * proofreading. Eliminate flicker with removal of super.paint(). \ in * bat now show in special font. * 1.5 2004-06-01 slightly larger margins, use new * BatTokenizer, HTMLTokenizer, JavaTokenizer * 1.6 2004-07-16 better recovery when cannot read *.ser * file. * 1.7 2005-06-12 destroy, make sure not null before remove. * Futures implement my own copy/paste that works with Swing or AWT token * needs to remember where it is on screen. * 1.8 2005-07-28 major overhaul to use new tokenizers.. * 1.9 2005-09-07 allow JDisplay to run under Eclipse * 2.0 2005-11-11 make snippet/ optional in Applet url * parameter. * 2.1 2005-12-25 add parser for *.properties files * 2.2 2005-12-25 add parser for *.csv files * 2.3 2005-12-25 add parser for *.ini files * 2.4 2005-12-25 more robust error handling * 2.5 2006-01-27 prints vm version, more checks. * 2.6 2006-03-06 reformat with IntelliJ and add Javadoc * 2.7 2007-04-29 use a corresponding mono font when turn off colour. * 2.8 2007-05-05 add iformat rendering, use of snippet/ser and snippet/iformat * 2.9 2007-07-12 first public distribution. * 3.0 2007-07-26 add support for annotations. * 3.1 2007-08-20 new colour scheme. * 3.2 2007-09-17 rename snippets -> snippet. Label *.java and *.javafrag properly. * 3.3 2008-01-11 add support for hex and octal numerics. * 3.4 2008-02-23 bold variable definitions. more robust display of class on dump. * 3.5 2008-02-24 change sizes and spacing * 3.6 2008-03-06 convert to Swing * 3.7 2008-04-18 get JDisplay and CSS font renderings in closer sync * 3.8 2008-04-30 improve way numeric literals are rendered in Java. * 3.9 2008-08-08 add vanilla text parser for text files. No changes needed to JDisplay itself, just the bundle. * 4.0 2009-04-12 shorter style names, improved highlighting. * 4.1 2009-08-30 tone down colour for keywords. * 4.2 2009-09-30 fine tune size of fonts, adjusting large or small fonts to normal size, shrink keyword size. * 4.3 2010-02-08 highlight begin and ends of comments and CDATAs specially. * 4.4 2010-02-10 add manifest tokenizer. * 4.5 2011-02-03 tidy up code, fix bug stopping B&W version from scrolling. * 4.6 2014-04-17 must be signed. Reads files and sets LAF. * 4.7 2014-08-01 change *.html to *.htm, *.adler to *.checksum, use 64bit FNV1a64 checksums. */ package com.mindprod.jdisplay; import com.mindprod.common18.Build; import com.mindprod.common18.Common18; import com.mindprod.common18.FontFactory; import com.mindprod.common18.HybridJ; import com.mindprod.common18.JEButton; import com.mindprod.common18.Misc; import com.mindprod.common18.ST; import com.mindprod.common18.VersionCheck; import com.mindprod.jtokens.Token; import com.mindprod.jtokens.TokenFonts; import javax.swing.JApplet; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JScrollPane; import java.awt.Color; import java.awt.Container; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.IOException; import java.io.InputStream; import java.io.InvalidClassException; import java.io.ObjectInputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.zip.GZIPInputStream; import static java.lang.System.*; /** * Displays a serialised pre-parsed Java program in fancy fonts and colours. *

* takes parm url = relative or absolute url of * the *.java file without the .ser. *

* jdisplay is an Applet to render large snippets. jdisplayaux handles inserting code into the HTML for htmlmacros. * jprep parses the snippet. * * @author Roedy Green, Canadian Mind Products * @version 4.7 2014-08-01 change *.html to *.htm, *.adler to *.checksum, use 64bit FNV1a64 checksums. * @see com.mindprod.jdisplayaux.WaysToRender * @since 2004 */ public final class JDisplay extends JApplet { /** * true if want additional debugging info */ private static final boolean DEBUGGING = false; /** * should we use line numbers? */ private static final boolean HAS_LINE_NUMBERS_DEFAULT = false; /** * height of Applet box in pixels. Does not include surrounding frame. Only useful when run as an application. */ private static final int APPLET_HEIGHT = 720; /** * Width of Applet box in pixels. Only useful when run as an application. */ private static final int APPLET_WIDTH = 960; private static final int FIRST_COPYRIGHT_YEAR = 2004; /** * version number for this class */ private static final long serialVersionUID = 30; /** * undisplayed copyright notice * * @noinspection UnusedDeclaration */ private static final String EMBEDDED_COPYRIGHT = "Copyright: (c) 2004-2017 Roedy Green, Canadian Mind Products, http://mindprod.com"; /** * @noinspection UnusedDeclaration */ private static final String RELEASE_DATE = "2014-08-01"; /** * Title */ private static final String TITLE_STRING = "JDisplay"; /** * Version, is no About box. */ private static final String VERSION_STRING = "4.7"; /** * used for background of bar userWantsColor */ private static final Color BACKGROUND_FOR_APPLET = Build.BACKGROUND_FOR_BLENDING; /** * true if running as Applet, false as as application. */ private final boolean asApplet; /** * plain B & W TextArea to display on for copy/paste. */ private BWTokenPanel bwTokenPanel; /** * Display with colours, as bit image. */ private ColorTokenPanel colorTokenPanel; /** * contentPane for JApplet, not local as usual. */ private Container contentPane; /** * payload size information. */ private Footprint footprint; /** * button to click to download source. */ private JButton download; /** * check to display in userWantsColor/B&W. */ private JCheckBox userWantsColor; /** * use Line numbers? End user decides */ private JCheckBox userWantsLineNumbers; /** * scrolls BWTokenPanel */ private JScrollPane bwTokenPanelScroller; /** * scrolls colorTokenPanel */ private JScrollPane colorTokenPanelScroller; /** * e.g. Myprog.java (no lead snippet/ser or trail .ser . */ private String snippetName; /** * The list of tokens */ private Token[] tokens; /** * true if want control bar on top. Controlled by an Applet bar parameter. Nearly always true. */ private boolean hasBar = true; /** * true after bwTokenPanel has been loaded with tokens */ private boolean hasBWPlainTextBeenLoaded = false; /** * Default constructor when started as an Applet. */ public JDisplay() { this.asApplet = true; this.hasBar = true; } /** * Constructor for when running from command line. * * @param snippetName bare name of snippet, no lead snippet/ser or trailing .ser */ private JDisplay( String snippetName ) { this.snippetName = snippetName; this.asApplet = false; this.hasBar = true; } /** * hook up the listeners */ private void addListeners() { // if click on pretty image, get plain textarea can be cut/pasted colorTokenPanel.addMouseListener( new MouseAdapter() { /** * If user clicks colour display as if to copy /paste, flip to bw * @param event details of event */ public void mouseClicked( MouseEvent event ) { userWantsColor.setSelected( false ); userWantsLineNumbers.setSelected( false ); redisplay(); } // end mouseClicked } // end anonymous class );// end addMouseListener line ItemListener theListener = new ItemListener() { /** * Notice any change to one of the list box selectors. * @param event details of just what the user clicked. */ public void itemStateChanged( ItemEvent event ) { // we are interested it both SELECTED and DESELECTED events redisplay(); } }; // hook up so display will change if any widgets touched. userWantsColor.addItemListener( theListener ); // hook up so display will change if any widgets touched. userWantsLineNumbers.addItemListener( theListener ); download.addActionListener( new ActionListener() { /** * Notice any change to one of the list box selectors. * @param event details of just what the user clicked. */ public void actionPerformed( ActionEvent event ) { download.setEnabled( false ); download(); download.setEnabled( true ); } // end actionPerformed } ); } /** * allocate GUI components */ private void buildComponents() { userWantsLineNumbers = new JCheckBox( "line numbers", HAS_LINE_NUMBERS_DEFAULT ); userWantsColor = new JCheckBox( "Color", true ); download = new JEButton( "download" ); // leave background default, smaller text that usual download.setFont( FontFactory.build( "Dialog", Font.BOLD, 12 ) ); // Allocate the BWTokenPanel now, but don't populate it with tokens unless // we have to. bwTokenPanel = new BWTokenPanel(); bwTokenPanel.setBackground( Color.WHITE ); bwTokenPanel.setFont( FontFactory.build( "monospaced", Font.PLAIN, TokenFonts.NORMAL_FONT_SIZE_IN_POINTS ) ); // don't load bwTokenPanel with tokens until until needed. bwTokenPanel.setVisible( false ); colorTokenPanel = new ColorTokenPanel(); colorTokenPanel.setBackground( Color.WHITE ); colorTokenPanel.setVisible( false ); // gets tokens fetchTokens(); colorTokenPanel.setTokens( tokens, footprint.totalLines ); // Decide if we need scrollbars // See if it will fit without: // Recompute with our font metrics footprint.s2CalcPayloadFootprint( tokens, this ); // footprint.s3CalcFat( tokens ) not needed since we are rendering with Applet. footprint.s4CalcScrollableFootprint( Rendering.APPLET ); footprint.s5CalcIdealAppletFootPrint( Rendering.APPLET, hasBar, HAS_LINE_NUMBERS_DEFAULT, false /* hscroll */, false /* vscroll */, 1.0f /* no safety factor, we know exact metrics now */ ); boolean horBars = this.getWidth() < footprint.idealAppletWidth; boolean vertBars = this.getHeight() < footprint.idealAppletHeight; // we dont set values in the colorTokenPanel until later, in // start/redisplay. colorTokenPanelScroller = new JScrollPane( colorTokenPanel, vertBars ? JScrollPane.VERTICAL_SCROLLBAR_ALWAYS : JScrollPane.VERTICAL_SCROLLBAR_NEVER, horBars ? JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS : JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); // controls how fast you scroll with the wheelmouse. colorTokenPanelScroller.getVerticalScrollBar().setUnitIncrement( Geometry.LEADING_PX ); colorTokenPanelScroller.setVisible( false ); bwTokenPanelScroller = new JScrollPane( bwTokenPanel, vertBars ? JScrollPane.VERTICAL_SCROLLBAR_ALWAYS : JScrollPane.VERTICAL_SCROLLBAR_NEVER, horBars ? JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS : JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); // controls how fast you scroll with the wheelmouse. bwTokenPanelScroller.getVerticalScrollBar().setUnitIncrement( Geometry.LEADING_PX ); bwTokenPanelScroller.setVisible( false ); // both b&w and Color are invisible at this point. } /** * download java source, DOWNLOAD not display. Let user capture to disk. cannot do FileChooser and Write in unsigned * applet. so get browser to do it. */ private void download() { try { final URL url; if ( asApplet ) { // get original document, not the *.ser url = new URL( getDocumentBase(), "snippet/" + snippetName ); getAppletContext().showDocument( url ); } else { out.println( "download ignored in local mode." ); // url = new URL( snippetName ); // we ignore the download if running standalone. // Ideally we should do some download dialog here. } } catch ( MalformedURLException e ) { err.println(); e.printStackTrace( err ); // no .ser, this is the human readable fragment err.println( "\007problem downloading " + snippetName ); err.println(); } } // end download /** * Gets an array of pre-parsed serialized tokens from website representing this program. Gets from snippetName. * Leaves the array in tokens. */ private void fetchTokens() { // Read serialised tokens from a compressed URL. tokens = null; // O P E N // Generate an HTTP GET Command final ObjectInputStream ois; footprint = null; try { // O P E N final URL url; // get corresponding *.ser if ( asApplet ) { url = new URL( getDocumentBase(), "snippet/ser/" + snippetName + ".ser" ); } else { url = new URL( "file:snippet/ser/" + snippetName + ".ser" ); } // out.println( "fetching: " + url ); final URLConnection urlc = url.openConnection(); if ( urlc == null ) { throw new IOException( "\007ailed to connect to document server." ); } urlc.setAllowUserInteraction( false ); urlc.setDoInput( true ); urlc.setDoOutput( false ); urlc.setUseCaches( false ); urlc.connect(); // ignored if already connected. final InputStream is = urlc.getInputStream(); final GZIPInputStream gzis = new GZIPInputStream( is, 4 * 1024/* buffsize */ ); ois = new ObjectInputStream( gzis ); // R E A D, footprintversion, footprint, tokens long expectedVersion = Footprint.serialVersionUID; long fileVersion = ( Long ) ois.readObject(); if ( fileVersion != expectedVersion ) { err.println( "\007Stale " + snippetName + " *.ser files are version " + fileVersion + ". JDisplay is expecting " + expectedVersion ); ois.close(); tokens = new Token[ 0 ]; return; } // we have to recompute it with our font metrics, but we want the // totalLines count. footprint = ( Footprint ) ois.readObject(); tokens = ( Token[] ) ois.readObject(); // C L O S E ois.close(); } catch ( InvalidClassException e ) { err.println( "\007Stale " + snippetName ); } catch ( ClassNotFoundException e ) { err.println( "\007Bug: Token class files missing from jar " + e.getMessage() ); } catch ( IOException e ) { err.println(); e.printStackTrace( err ); err.println( "\007Problem getting compacted source document " + snippetName ); err.println(); } if ( tokens == null ) { tokens = new Token[ 0 ]; } } /** * Get Applet optional boolean parameter * * @param paramName Name of the parameter. Case insensitive. * @param defaultValue default if param is missing. * * @return Value of the parameter from the Applet true or false * @noinspection SameParameterValue */ private boolean getBooleanParameter( String paramName, boolean defaultValue ) { String boolString = getParameter( paramName ); if ( ST.isEmpty( boolString ) ) { return defaultValue; } else { return Misc.parseBoolean( boolString, false ); } } /** * Get parameters from Applet, but only when running as Applet */ private void getParams() { // We are in Applet, don't have parms yet. this.snippetName = getParameter( "snippet" ); if ( this.snippetName == null ) { throw new IllegalArgumentException( "missing snippet parameter" ); } // should not have leading snippet/ser/ if ( this.snippetName.startsWith( "snippet/" ) ) { this.snippetName = this.snippetName.substring( "snippet/".length() ); } if ( this.snippetName.startsWith( "ser/" ) ) { this.snippetName = this.snippetName.substring( "ser/".length() ); } // in Java 1.6 the above code fails to prepend. // can't use assert. This code has to compile under JDK 1.2 hasBar = getBooleanParameter( "bar", true ); } /** * layout components */ private void layoutGridBag() { /* layout * ----0--------1--------2-------- * userWantsColor--userWantsLineNumbers--download-- 0 * pretty ------------------------ 1 * plain ------------------------- 2 */ if ( hasBar ) { /* * The bar is not a component, just the camouflage Applet background * showing through. */ // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( userWantsColor, new GridBagConstraints( 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, new Insets( 0, 0, 2, 0 ), 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( userWantsLineNumbers, new GridBagConstraints( 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.NORTH, GridBagConstraints.NONE, new Insets( 0, 10, 2, 0 ), 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( download, new GridBagConstraints( 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHEAST, GridBagConstraints.NONE, new Insets( 0, 10, 2, 0 ), 0, 0 ) ); } // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( colorTokenPanelScroller, new GridBagConstraints( 0, 1, 3, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets( 0, 0, 0, 0 ), 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( bwTokenPanelScroller, new GridBagConstraints( 0, 2 /* place it beside colorTokenPanelScroller, though actually only one visible at a time */, 3, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets( 0, 0, 0, 0 ), 0, 0 ) ); } /** * refresh the display based on whether should use userWantsColor and userWantsLineNumbers */ private void redisplay() { final boolean useColor = userWantsColor.isSelected(); final boolean useLineNumbers = this.userWantsLineNumbers.isSelected(); // can't have line numbers in B&W state, user would not want to copy/paste them. final int width = useLineNumbers ? footprint.scrollableWidthWithLineNumbers : footprint.scrollableWidthWithoutLineNumbers; final int height = footprint.scrollableHeight; /* * Tell the colorTokenPanel how we want the tokens rendered. */ if ( useColor ) { // leave B&W connected, just not visible. colorTokenPanel.set( width, height, useLineNumbers, footprint.lineNumberWidthInPixels ); // invalidates this.userWantsLineNumbers.setEnabled( true ); this.userWantsLineNumbers.setVisible( true ); bwTokenPanelScroller.setVisible( false ); colorTokenPanelScroller.setVisible( true ); bwTokenPanel.setVisible( false ); colorTokenPanel.setVisible( true ); } else { // B&W // Leave userWantsColor connected, just not visible. this.userWantsLineNumbers.setEnabled( false ); this.userWantsLineNumbers.setVisible( false ); this.userWantsLineNumbers.setSelected( false ); if ( !hasBWPlainTextBeenLoaded ) { bwTokenPanel.setTokens( tokens ); hasBWPlainTextBeenLoaded = true; } colorTokenPanelScroller.setVisible( false ); bwTokenPanelScroller.setVisible( true ); colorTokenPanel.setVisible( false ); bwTokenPanel.setVisible( true ); } // no need for invalidate/validate/repaint. } /** * Allow this Applet to run as as application as well. * * @param args url of text file to display e.g. abs.example1.javafrag . CWD must be E:\mindprod\jgloss\ */ public static void main( String args[] ) { if ( args.length == 0 ) { throw new IllegalArgumentException( "missing snippet parameter" ); } HybridJ.fireup( new JDisplay( args[ 0 ] ), TITLE_STRING + " " + VERSION_STRING, APPLET_WIDTH, APPLET_HEIGHT ); } // end main /** * layout the components */ /** * Called by the browser or Applet viewer to inform * this Applet that it is being reclaimed and that it should destroy * any resources that it has allocated. */ public void destroy() { if ( userWantsColor != null ) { contentPane.remove( userWantsColor ); userWantsColor = null; } if ( download != null ) { contentPane.remove( download ); download = null; } if ( userWantsLineNumbers != null ) { contentPane.remove( userWantsLineNumbers ); userWantsLineNumbers = null; } if ( colorTokenPanelScroller != null && colorTokenPanel != null ) { colorTokenPanelScroller.remove( colorTokenPanel ); colorTokenPanel = null; } if ( colorTokenPanelScroller != null ) { contentPane.remove( colorTokenPanelScroller ); colorTokenPanelScroller = null; } if ( bwTokenPanelScroller != null && bwTokenPanel != null ) { bwTokenPanelScroller.remove( bwTokenPanel ); bwTokenPanel = null; } if ( bwTokenPanelScroller != null ) { contentPane.remove( bwTokenPanelScroller ); bwTokenPanelScroller = null; } } /** * Called by the browser or Applet viewer to inform * this Applet that it has been loaded into the system. */ @Override public void init() { if ( !VersionCheck.isJavaVersionOK( 1, 8, 0, this ) ) { // effectively abort return; } Common18.setLaf(); // helps track bugs to know version customer was using if ( DEBUGGING ) { out.println( "initialising " + TITLE_STRING + " " + VERSION_STRING + " released:" + RELEASE_DATE + " build:" + Build.BUILD_NUMBER + " in Java " + System.getProperty( "java.version", "unknown" ) ); } if ( asApplet ) { getParams(); } contentPane = this.getContentPane(); contentPane.setLayout( new GridBagLayout() ); contentPane.setBackground( BACKGROUND_FOR_APPLET );// make it blend into CMP background buildComponents(); // add components: layoutGridBag(); addListeners(); this.validate(); this.setVisible( true ); } // end init /** * make sure the pretty version is displaying. */ public void start() { if ( userWantsColor != null ) { userWantsColor.setSelected( true ); redisplay(); } } }