/* * [BraceBalancer.java] * * Summary: Helps you find unbalanced braces, brackets and parentheses in any language. * * Copyright: (c) 2010-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.0 2010-01-05 initial version. * 1.1 2011-01-06 add sound effects, fix bugs. * 1.2 2011-11-20 add prompts and sound effects to make handling very large documents less confusing. * 1.3 2012-03-23 choice of Balancer, export button */ package com.mindprod.bracebalancer; import com.mindprod.common18.Build; import com.mindprod.common18.CMPAboutJBox; import com.mindprod.common18.ClipboardPoker; import com.mindprod.common18.EIO; import com.mindprod.common18.FontFactory; import com.mindprod.common18.HybridJ; import com.mindprod.common18.JEButton; import com.mindprod.common18.Laf; import com.mindprod.common18.Misc; import com.mindprod.common18.Play; import com.mindprod.common18.ST; import com.mindprod.common18.VersionCheck; import com.mindprod.entities.EntifyStrings; import com.mindprod.fastcat.FastCat; import com.mindprod.hunkio.HunkIO; import javax.swing.DefaultComboBoxModel; import javax.swing.JApplet; import javax.swing.JComboBox; import javax.swing.JEditorPane; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; 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.datatransfer.Clipboard; import java.awt.datatransfer.ClipboardOwner; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.io.File; import java.io.IOException; import static java.lang.System.*; /** * Helps you find unbalanced braces, brackets and parentheses in any language. *

* This is both an Applet and a standalone application. * * @author Roedy Green, Canadian Mind Products * @version 1.3 2012-03-23 choice of Balancer, export button * @since 2010-12-27 */ public final class BraceBalancer extends JApplet implements ClipboardOwner, Runnable { /** * if true extra debug output. */ private static final boolean DEBUGGING = false; /** * height of Applet in pixels. Does not include bar at top, but does include menu bar. */ private static final int APPLET_HEIGHT = 900; /** * Width of Applet in pixels. */ private static final int APPLET_WIDTH = 758; private static final int FIRST_COPYRIGHT_YEAR = 2010; /** * DOCTYPE for generated HTML */ private static final String DOCTYPE = ""; /** * undisplayed copyright notice * * @noinspection UnusedDeclaration */ private static final String EMBEDDED_COPYRIGHT = "Copyright: (c) 2010-2017 Roedy Green, Canadian Mind Products, http://mindprod.com"; /** * CrLf or lf used for line endings */ private static final String lineSeparator = System.getProperty( "line.separator" ); /** * date this version released. */ private static final String RELEASE_DATE = "2012-03-23"; /** * want a mono font so code will align. */ private static final String stylesheet = "\n"; /** * Title of the Applet */ private static final String TITLE_STRING = "Brace Balancer"; /** * Version of the Applet */ private static final String VERSION_STRING = "1.3"; /** * background colour, pale green to match website */ private static final Color BACKGROUND_FOR_BODY = Build.BACKGROUND_FOR_BLENDING; /** * background where for data entry where user enters a value. */ private static final Color BACKGROUND_FOR_EDITABLE = Color.WHITE; /** * data entry text field colour */ private static final Color FOREGROUND_FOR_EDITABLE = Color.BLACK; private static final Color FOREGROUND_FOR_INSTRUCTIONS = new Color( 0x008000 ); private static final Color FOREGROUND_FOR_PROGRESS = new Color( 0x000080 ); /** * for titles */ private static final Color FOREGROUND_FOR_TITLE = new Color( 0xdc143c ); /** * title font */ private static final Font FONT_FOR_TITLE = FontFactory.build( "Dialog", Font.BOLD, 15 ); /** * for for title second line */ private static final Font FONT_FOR_TITLE2 = FontFactory.build( "Dialog", Font.PLAIN, 14 ); private static final Font INSTRUCTIONS_FONT = FontFactory.build( "Dialog", Font.PLAIN, 13 ); private static final Font PROGRESS_FONT = FontFactory.build( "Dialog", Font.PLAIN, 14 ); /** * which balancer the user selected. */ private Balancer balancer; /** * contentPane for the whole Applet */ private Container contentPane; /** * which syntactic elements to balance. */ private JComboBox whatToBalance; /** * invoke colourising. */ private JEButton export; /** * invoke colourising. */ private JEButton paste; /** * display instructions for use */ private JEditorPane instructions; /** * where we display the code with unbalanced delimiter */ private JEditorPane jep; /** * let user know progress of colorising */ private JLabel progress; /** * name of app */ private JLabel title; /** * build and version */ private JLabel title2; /** * scrollpane to hold the JEditorPane */ private JScrollPane jepScrollPane; /** * the text from the clipboard after CSS/HTML formatting added */ private String formattedProgramText; /** * clipboard contents before we colourise it */ private String rawClipboard; private boolean maxNestingDepthsComputed = false; private int maxBraceNesting = 0; private int maxBracketNesting = 0; private int maxParenthesisNesting = 0; /** * previous background colour we rendered */ private int prevrgb; /** * Build the left margin spanner chars to show span of various markers * * @param nestingDepth current nesting depth * @param maxNestingDepth deepest depth in the whole program * * @return Some line drawing chars. */ private static String buildOneSpanner( final int nestingDepth, final int maxNestingDepth ) { assert nestingDepth <= maxNestingDepth : "inconsistent nestingDepth and maxNestingDepth" + nestingDepth + " " + maxNestingDepth; final StringBuilder sb = new StringBuilder( maxNestingDepth ); sb.append( ST.rep( '|', Math.max( 0, nestingDepth ) ) ); sb.append( ST.spaces( Math.max( 0, maxNestingDepth - nestingDepth ) ) ); return sb.toString(); } /** * build all the Swing components. */ private void buildComponents() { contentPane.setBackground( BACKGROUND_FOR_BODY ); title = new JLabel( TITLE_STRING + " " + VERSION_STRING ); title.setFont( FONT_FOR_TITLE ); title.setForeground( FOREGROUND_FOR_TITLE ); title2 = new JLabel( "released:" + RELEASE_DATE + " build:" + Build.BUILD_NUMBER ); title2.setFont( FONT_FOR_TITLE2 ); title2.setForeground( FOREGROUND_FOR_TITLE ); whatToBalance = new JComboBox<>(); // turn off the write-in feature. whatToBalance.setEditable( false ); whatToBalance.setModel( new DefaultComboBoxModel<>( Balancer.values() ) ); whatToBalance.setSelectedItem( Balancer.ALL ); balancer = Balancer.ALL; jep = new JEditorPane(); jep.setEditable( false ); jep.setContentType( "text/html" ); jep.setForeground( FOREGROUND_FOR_EDITABLE ); jep.setBackground( BACKGROUND_FOR_EDITABLE ); jep.setMargin( new Insets( 5, 5, 5, 5 ) ); jepScrollPane = new JScrollPane( jep, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ); final FastCat sb = new FastCat( 6 ); // cannot use any css, HTML 3.2 only sb.append( "" ); sb.append( "" ); sb.append( "

" ); sb.append( "" ); final String instructionsHTML = sb.toString(); instructions = new JEditorPane(); instructions.setContentType( "text/html" ); instructions.setEditable( false ); instructions.setMargin( new Insets( 5, 5, 5, 5 ) ); instructions.setFont( INSTRUCTIONS_FONT ); instructions.setForeground( FOREGROUND_FOR_INSTRUCTIONS ); try { instructions.setText( instructionsHTML ); } catch ( Exception e ) { Play.play( BraceBalancer.class, "sound/echo.au" ); err.println( "Because of Sun bug, unable to display instructions, please exit browser and restart" ); e.printStackTrace( err ); err.println( instructionsHTML ); } paste = new JEButton( "Paste" ); progress = new JLabel(); progress.setFont( PROGRESS_FONT ); progress.setForeground( FOREGROUND_FOR_PROGRESS ); export = new JEButton( "Export" ); } /** * build a menu with Look & Feel and About across the top */ private void buildMenu() { // turn on anti-alias System.setProperty( "swing.aatext", "true" ); final JMenuBar menubar = new JMenuBar(); setJMenuBar( menubar ); final JMenu lafMenu = Laf.buildLookAndFeelMenu(); if ( lafMenu != null ) { menubar.add( lafMenu ); } final JMenu menuHelp = new JMenu( "Help" ); menubar.add( menuHelp ); final JMenuItem aboutItem = new JMenuItem( "About" ); menuHelp.add( aboutItem ); aboutItem.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { // open about frame new CMPAboutJBox( Misc.getParentFrame( BraceBalancer.this ), TITLE_STRING, VERSION_STRING, "Helps you balance mismatched {} [] () in source code.", "", "freeware", RELEASE_DATE, FIRST_COPYRIGHT_YEAR, "Roedy Green", "BRACEBALANCER", "1.8" ); } } ); } /** * build spanner lines for left margin. * * @param braceNesting current {} nesting * @param bracketNesting current [] nesting * @param parenthesisNesting current () nesting * @param balancer what we are currently balancing. * * @return some || chars to indicate depth. */ private String buildSpanner( int braceNesting, int bracketNesting, int parenthesisNesting, Balancer balancer ) { final String r; switch ( balancer ) { case ALL: return ""; case BRACE: r = buildOneSpanner( braceNesting, maxBraceNesting ); break; case BRACKET: r = buildOneSpanner( bracketNesting, maxBracketNesting ); break; case PARENTHESIS: r = buildOneSpanner( parenthesisNesting, maxParenthesisNesting ); break; default: throw new IllegalArgumentException( "BraceBalancer program bug. unknown Balancer: " + balancer ); } return "" + r + ""; } /** * calculate the maximum nesting depths * * @param big string to parse and colourise */ private void calcMaxNestingDepths( final String big ) { maxBraceNesting = 0; maxBracketNesting = 0; maxParenthesisNesting = 0; int braceNesting = 0; int bracketNesting = 0; int parenthesisNesting = 0; boolean inDoubleQuotes = false; boolean inSingleQuotes = false; boolean inQuotes = false; char prevChar = 0; for ( int i = 0; i < big.length(); i++ ) { final char c = big.charAt( i ); switch ( c ) { case '{': if ( !inQuotes ) { braceNesting++; if ( braceNesting > maxBraceNesting ) { maxBraceNesting = braceNesting; } } break; case '}': if ( !inQuotes ) { braceNesting--; } break; case '[': if ( !inQuotes ) { bracketNesting++; if ( bracketNesting > maxBracketNesting ) { maxBracketNesting = bracketNesting; } } break; case ']': if ( !inQuotes ) { bracketNesting--; } break; case '(': if ( !inQuotes ) { parenthesisNesting++; if ( parenthesisNesting > maxParenthesisNesting ) { maxParenthesisNesting = parenthesisNesting; } } break; case ')': if ( !inQuotes ) { parenthesisNesting--; } break; case '\'': if ( !inDoubleQuotes && prevChar != '\\' ) { inSingleQuotes = !inSingleQuotes; inQuotes = inSingleQuotes || inDoubleQuotes; } break; case '\"': if ( !inSingleQuotes && prevChar != '\\' ) { inDoubleQuotes = !inDoubleQuotes; inQuotes = inSingleQuotes || inDoubleQuotes; } break; default: } prevChar = c; } if ( DEBUGGING ) { out.println( "maxBraceNesting:" + maxBraceNesting + " maxBracketNesting:" + maxBracketNesting + " " + "maxParenthesisNesting:" + maxParenthesisNesting ); } } /** * colourise backgrounds according to the nesting depth */ private void colouriseAndDisplayClipboard() { assert balancer != null : "balancer not set"; progress.setText( "Collecting the clipboard. Please be patient..." ); // contents of the clipboard before processing. rawClipboard = ClipboardPoker.getClip( this ); maxNestingDepthsComputed = false; if ( rawClipboard != null && rawClipboard.length() > 0 ) { progress.setText( "Colourising the clipboard. Please be patient..." ); Play.play( BraceBalancer.class, "sound/cork.au" ); // fire up run on separate thread to parse the clipboard and render it new Thread( this ).start(); // run on new thread } else { progress.setText( "Empty Clipboard..." ); Play.play( BraceBalancer.class, "sound/echo.au" ); err.println( "empty clipboard" ); } } /** * colourise backgrounds according to the nesting depth * * @param big string to parse and colourise * @param balancer which balancer to display * * @return string with embedded html-colourising commands. */ private String colouriseProgramText( final String big, final Balancer balancer ) { prevrgb = 0xffffff; /* white */ int braceNesting = 0; int bracketNesting = 0; int parenthesisNesting = 0; boolean inDoubleQuotes = false; boolean inSingleQuotes = false; boolean inQuotes = false; char prevChar = 0; // we insist comments balance. final StringBuilder sb = new StringBuilder( big.length() * 5 ); sb.append( DOCTYPE ); sb.append( "" ); sb.append( stylesheet ); // in theory this should not work. Only HTML 3.2 guaranteed to be supported. sb.append( "
" );
        // do spanner for first line.
        sb.append( buildSpanner( braceNesting, bracketNesting, parenthesisNesting, balancer ) );
        for ( int i = 0; i < big.length(); i++ )
            {
            final char c = big.charAt( i );
            switch ( c )
                {
                case '\r':
                    /* ignore, will be reinserted when see \n */
                    break;
                case '\n':
                    sb.append( lineSeparator );
                    sb.append( buildSpanner( braceNesting, bracketNesting, parenthesisNesting, balancer ) );
                    break;
                case '{':
                    if ( !inQuotes )
                        {
                        braceNesting++;
                        sb.append( newColour( braceNesting, bracketNesting, parenthesisNesting, balancer ) );
                        }
                    sb.append( c );
                    break;
                case '}':
                    if ( !inQuotes )
                        {
                        sb.append( c );
                        braceNesting--;
                        sb.append( newColour( braceNesting, bracketNesting, parenthesisNesting, balancer ) );
                        }
                    else
                        {
                        sb.append( c );
                        }
                    break;
                case '[':
                    if ( !inQuotes )
                        {
                        bracketNesting++;
                        sb.append( newColour( braceNesting, bracketNesting, parenthesisNesting, balancer ) );
                        }
                    sb.append( c );
                    break;
                case ']':
                    if ( !inQuotes )
                        {
                        sb.append( c );
                        bracketNesting--;
                        sb.append( newColour( braceNesting, bracketNesting, parenthesisNesting, balancer ) );
                        }
                    else
                        {
                        sb.append( c );
                        }
                    break;
                case '(':
                    if ( !inQuotes )
                        {
                        parenthesisNesting++;
                        sb.append( newColour( braceNesting, bracketNesting, parenthesisNesting, balancer ) );
                        }
                    sb.append( c );
                    break;
                case ')':
                    if ( !inQuotes )
                        {
                        sb.append( c );
                        parenthesisNesting--;
                        sb.append( newColour( braceNesting, bracketNesting, parenthesisNesting, balancer ) );
                        }
                    else
                        {
                        sb.append( c );
                        }
                    break;
                case '\'':
                    if ( !inDoubleQuotes && prevChar != '\\' )
                        {
                        inSingleQuotes = !inSingleQuotes;
                        inQuotes = inSingleQuotes || inDoubleQuotes;
                        }
                    sb.append( '\'' );
                    break;
                case '\"':
                    if ( !inSingleQuotes && prevChar != '\\' )
                        {
                        inDoubleQuotes = !inDoubleQuotes;
                        inQuotes = inSingleQuotes || inDoubleQuotes;
                        }
                    sb.append( """ );
                    break;
                default:
                    sb.append( EntifyStrings.toHTMLEntity( c ) );
                }
            prevChar = c;
            }
        sb.append( "
" ); return sb.toString(); } /** * hook up the listeners */ private void hookListeners() { paste.addActionListener( new ActionListener() { public void actionPerformed( final ActionEvent e ) { colouriseAndDisplayClipboard(); } } ); // whatToBalance.addItemListener( new ItemListener() { /** * Notice any change to one of the list box selectors * @param e details of just what the user clicked. */ public void itemStateChanged( ItemEvent e ) { if ( e.getStateChange() == ItemEvent.SELECTED ) { recolouriseAndDisplayClipboard(); } } } ); // // what to do when user clicks the export button. export.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { final JFileChooser fc = new JFileChooser(); // No filter. No suggested starting point. switch ( fc.showOpenDialog( BraceBalancer.this ) ) { case JFileChooser.APPROVE_OPTION: final File file = fc.getSelectedFile(); fc.setFileSelectionMode( JFileChooser.FILES_ONLY ); try { HunkIO.writeEntireFile( file, formattedProgramText, HunkIO.UTF8 ); progress.setText( "Formatted program text exported as HTML to " + EIO.getCanOrAbsPath ( file ) ); } catch ( IOException ioe ) { progress.setText( "Unable to export formatted program" ); } break; case JFileChooser.CANCEL_OPTION: case JFileChooser.ERROR_OPTION: break; default: } } } ); } /** * layout fields in GridBag */ private void layoutComponents() { contentPane.setLayout( new GridBagLayout() ); // _____ 0_____________________________1_____2___---- // 0 Title------------------- ------title2---------- 0 // 1 whatToBalance 1 // 2 jep -------------------- -------------- ------- 1 // 3 instructions---------------------------export-- 2 // 4 progress-------------------------------paste--- 4 // // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( title, new GridBagConstraints( 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets( 10, 10, 5, 5 ), // top, left, bottom, right 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( title2, new GridBagConstraints( 1, 0, 2, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 10, 5, 5, 10 ), // top, left, bottom, right 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( whatToBalance, new GridBagConstraints( 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets( 5, 10, 5, 5 ), // top, left, bottom, right 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( jepScrollPane, new GridBagConstraints( 0, 2, 3, 1, 100.0, 90.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets( 5, 10, 5, 10 ), // top, left, bottom, right 0, 0 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( instructions, new GridBagConstraints( 0, 3, 2, 2, 90.0, 5.0, GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets( 5, 10, 10, 5 ), 40, 10 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( export, new GridBagConstraints( 2, 3, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 5, 5, 5, 10 ), 10, 10 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( paste, new GridBagConstraints( 2, 4, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 5, 5, 5, 10 ), 10, 10 ) ); // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( progress, new GridBagConstraints( 0, 5, 2, 1, 100.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets( 5, 10, 10, 10 ), // top, left, bottom, right 40, 0 ) ); } /** * compute HTML to generate a new colour based on nesting depth * * @param braceNesting inside how many {} * @param bracketNesting inside how many [] * @param parenthesisNesting inside how many () * @param balancer which balancer to display * * @return HTML to change the background colour */ private String newColour( int braceNesting, int bracketNesting, int parenthesisNesting, Balancer balancer ) { if ( braceNesting > 5 ) { braceNesting = 5; } if ( bracketNesting > 5 ) { bracketNesting = 5; } if ( parenthesisNesting > 5 ) { parenthesisNesting = 5; } final int r; final int g; final int b; final int rgb; switch ( balancer ) { case ALL: if ( braceNesting < 0 || bracketNesting < 0 || parenthesisNesting < 0 ) { rgb = 0xffff00; /* bright yellow */ } else { r = 255 - 24 * braceNesting; g = 255 - 24 * bracketNesting; b = 255 - 24 * parenthesisNesting; rgb = r << 16 | g << 8 | b; } break; case BRACE: if ( braceNesting < 0 ) { rgb = 0xffff00; /* bright yellow */ } else if ( braceNesting == 0 ) { rgb = 0xffffff; } else { int depth = 100 / Math.max( 1, maxBraceNesting ) * braceNesting; r = 255; // deeper red as nest. g = 255 - depth; b = 255 - depth; rgb = r << 16 | g << 8 | b; } break; case BRACKET: if ( bracketNesting < 0 ) { rgb = 0xffff00; /* bright yellow */ } else if ( bracketNesting == 0 ) { rgb = 0xffffff; } else { int depth = 100 / Math.max( 1, maxBracketNesting ) * bracketNesting; r = 255 - depth; g = 255; b = 255 - depth; rgb = r << 16 | g << 8 | b; } break; case PARENTHESIS: if ( parenthesisNesting < 0 ) { rgb = 0xffff00; /* bright yellow */ } else if ( parenthesisNesting == 0 ) { rgb = 0xffffff; } else { int depth = 100 / Math.max( 1, maxParenthesisNesting ) * parenthesisNesting; r = 255 - depth; g = 255 - depth; b = 255; rgb = r << 16 | g << 8 | b; } break; default: throw new IllegalArgumentException( "bad balancer enum " + balancer ); } if ( rgb == prevrgb ) { return ""; } else { prevrgb = rgb; return ""; } } /** * recolourise the clipboard previously extracted with different balancer */ private void recolouriseAndDisplayClipboard() { final Balancer oldBalancer = balancer; balancer = ( Balancer ) whatToBalance.getSelectedItem(); assert balancer != null : "null balancer"; if ( balancer != oldBalancer && rawClipboard != null && rawClipboard.length() > 0 ) { progress.setText( "Colourising the clipboard. Please be patient..." ); new Thread( this ).start(); // start up run on a new thread to } } /** * Allow this Applet to run as as application as well. * * @param args command line arguments ignored. */ public static void main( String args[] ) { HybridJ.fireup( new BraceBalancer(), TITLE_STRING + " " + VERSION_STRING, APPLET_WIDTH, APPLET_HEIGHT ); } // end main /** * 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. */ @Override public void destroy() { balancer = null; contentPane = null; paste = null; instructions = null; jep = null; jepScrollPane = null; progress = null; rawClipboard = null; title = null; title2 = null; whatToBalance = null; } /** * Called by the browser or Applet viewer to inform * this Applet that it has been loaded into the system. */ @Override public void init() { contentPane = getContentPane(); if ( !VersionCheck.isJavaVersionOK( 1, 7, 0, contentPane ) ) { // abort stop(); destroy(); } buildMenu(); // also initial L&F buildComponents(); layoutComponents(); hookListeners(); this.validate(); this.setVisible( true ); } /** * needed to let us use the clipboard */ public void lostOwnership( Clipboard clipboard, Transferable contents ) { /* ignore */ } /** * on a separate thread, prepare contents of clipboard for display */ public void run() { // this is very quick if ( !maxNestingDepthsComputed ) { calcMaxNestingDepths( rawClipboard ); maxNestingDepthsComputed = true; } formattedProgramText = colouriseProgramText( rawClipboard, balancer ); progress.setText( "" ); // now we have prepared the html, render the results SwingUtilities.invokeLater( new Runnable() { public void run() { try { // this takes quite a while jep.setText( formattedProgramText ); Play.play( BraceBalancer.class, "sound/start.au" ); // after this is done, the repaint is fast. } catch ( Exception e ) { Play.play( BraceBalancer.class, "sound/echo.au" ); err.println( "Unable to render that clipboard because of " + e.getMessage() ); err.println( "cooked: [" + formattedProgramText + "]" ); jep.setText( "Unable to render that clipboard" ); } } } ); } }