/*
 * TextScroll.java  2.8.3 15/12/98
 *
 * Copyright (c) 1997, 1998 by Kevin Swan.  All rights reserved.
 *
 * I reserve all rights to this software.  You may use it and
 * distribute it freely, provided you do not remove this header
 * and attribute partial credit to the author, Kevin Swan.
 *
 * 24 Feb 1998  Added directives to catch up to the Java 1.1 version.
 * 27 Feb 1998  Added setCenter() directive.
 *              Modified the supportedDirective() to use an array.
 *              Released as version 2.2.
 * 27 Feb 1998  Added setBold() and setItalic() directives.
 *              Added an element, inset, for the x coordinate
 *                of text to permit an inset.
 *              Added directive setInset() to set the inset.
 *              Released as version 2.3.
 * 31 Mar 1998  Modified to permit complete URLs in the "data"
 *                parameter of the applet.
 *              Released as version 2.4.
 * 01 Apr 1998  Added setURL() directive.
 *              Added version and credit information to init() method.
 *              Released as version 2.5.
 * 05 Apr 1998  Modified so that if a URL has been set using setURL(),
 *                then when the user moves their mouse over the applet,
 *                the target URL is displayed in the status bar, instead
 *                of "Click to start/stop the applet."  If no URL is set,
 *                it still displays "Click to ..."
 * 14 Apr 1998  Modified to display applet info in status bar instead of
 *                "Click to ..." if no URL is set.
 *              Released as version 2.5.2.
 * 14 Apr 1998  Modified to display messages while loading data, makes
 *                it a little more professional looking.
 *              Added the DataLoader class for asynchronous text data
 *                loading.
 *              Added the DirectiveManager class to decrease
 *                responsibility of the main class, and to work towards
 *                automatic text wrapping.
 *              Added the LineWrapManager class to take care of the
 *                line wrapping.
 *              Released as version 2.6.
 * 16 Apr 1998  Modified to permit specification of a target frame as
 *                well as a target URL.
 *              Released as version 2.7.
 * 20 Apr 1998  Did some major internal rewriting to correct the scrolling
 *                algorithm.  It is now more "correct."  Also lengthened
 *                the offscreen canvas to support font sizes of up to 200
 *                pixels.
 *              Added ability to refresh data from server.
 *              Expanded documentation.
 *              Released first version offered as Java 1.0 only.
 *              Released as version 2.8.
 * 21 Apr 1998  Added ability to alter the loading/error colors through
 *                <PARAM> tags.
 *              Modified to notify DataLoader thread when start() and
 *                stop() are called.
 *              Released as version 2.8.1.
 * 04 Jun 1998  Added ability to disable automatic line wrapping by
 *                including a <PARAM> tag.
 *              Was not officially released yet.
 * 06 Jun 1998  Modified setURL () to handle commas in valid URLs.
 *              Released as version 2.8.2.
 * 15 Dec 1998  Made some major modifications to allow a different
 *                colored panel on the left side to be displayed,
 *                similar to some other scrollers.
 *              Released as version 2.8.3.
 * 12 Jul 2004  Fixed a bug that caused a NullPointerException when the
 *              data refreshing failed.
 */



/*
 * Commented out for unbundled distribution.
 *
 * package kevin.applets.textscroll;
 */



import java.util.Vector;
import java.applet.*;
import java.awt.*;
import java.net.*;
import java.io.*;



/**
 * This applet is displays a two-paned scrolling text window.
 * The separate panes are right next to each other, but can
 * have different color schemes, to allow for a headline on
 * the left, with content on the right.  It is very simple in
 * design, but allows for a great deal of configuration.  The
 * user can specify multiple parameters with the
 * <CODE>&lt;PARAM&gt;</CODE> tags, such as the following:
 *
 * <P>
 * <TABLE WIDTH=100% BORDER>
 *   <TR>
 *     <TD BGCOLOR="#000000" WIDTH=20%>
 *       <FONT COLOR="#FFFFFF" SIZE=+1>Name</FONT>
 *     </TD>
 *     <TD BGCOLOR="#000000" WIDTH=40%>
 *       <FONT COLOR="#FFFFFF" SIZE=+1>Description</FONT>
 *     </TD>
 *     <TD BGCOLOR="#000000" WIDTH=40%>
 *       <FONT COLOR="#FFFFFF" SIZE=+1>Example</FONT>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>leftfontface</B></TD>
 *     <TD>The style of <CODE>Font</CODE> to use in the left
 *         pane.  It must be a type supported by Java.  If
 *         an invalid font face is specified,
 *         <CODE>SansSerif</CODE> will be used.
 *         <CODE>SansSerif</CODE> is also the default font face.
 *         Permitted values are:
 *         <UL>
 *           <LI>SansSerif
 *           <LI>Serif
 *           <LI>Monospaced
 *         </UL>
 *       <P>&nbsp;
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;leftfontface&quot;
 *                   VALUE=&quot;SansSerif&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>rightfontface</B></TD>
 *     <TD>The style of <CODE>Font</CODE> to use in the right
 *         pane.  It must be a type supported by Java.  If
 *         an invalid font face is specified,
 *         <CODE>SansSerif</CODE> will be used.
 *         <CODE>SansSerif</CODE> is also the default font face.
 *         Permitted values are:
 *         <UL>
 *           <LI>SansSerif
 *           <LI>Serif
 *           <LI>Monospaced
 *         </UL>
 *       <P>&nbsp;
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;rightfontface&quot;
 *                   VALUE=&quot;SansSerif&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>fontface</B></TD>
 *     <TD>The style of <CODE>Font</CODE> to use in the right
 *         pane.  This is the same as rightfontface, and was
 *         left in for backwards compatibility.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;fontface&quot;
 *                   VALUE=&quot;SansSerif&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>leftfontsize</B></TD>
 *     <TD>The size of <CODE>Font</CODE> to use in the left
 *         pane.  If an invalid size is specified, the default
 *         will be used.  The default font size is 10.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;leftfontsize&quot;
 *                   VALUE=&quot;12&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>rightfontsize</B></TD>
 *     <TD>The size of <CODE>Font</CODE> to use in the right
 *         pane.  If an invalid size is specified, the default
 *         will be used.  The default font size is 10.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;rightfontsize&quot;
 *                   VALUE=&quot;12&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>fontsize</B></TD>
 *     <TD>The size of <CODE>Font</CODE> to use in the right
 *         pane.  This is the same as rightfontsize, and was
 *         left in for backwards compatibility.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;fontsize&quot;
 *                   VALUE=&quot;12&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>speed</B></TD>
 *     <TD>The speed of scrolling to use.  Valid values are all numbers
 *         from <CODE>TextScroll.MIN_SPEED</CODE> (1) to
 *         <CODE>TextScroll.MAX_SPEED</CODE> (100), with 
 *         <CODE>TextScroll.MIN_SPEED</CODE> being the slowest and
 *         <CODE>TextScroll.MAX_SPEED</CODE> being the fastest.  The
 *         default is <CODE>TextScroll.DEFAULT_SPEED</CODE> (70).
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;speed&quot; VALUE=&quot;65&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>data</B></TD>
 *     <TD>The name of the datafile to use.
 *         In versions 2.4 and above, you can use a complete URL in this
 *         parameter if you wish.  If the value of this parameter starts
 *         with &quot;http&quot;, it tries to create a new URL from the
 *         string.  Otherwise, it treats it as a relative URL from the
 *         location of the HTML document containing the applet.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;data&quot; VALUE=&quot;scroll.txt&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>leftforeground</B></TD>
 *     <TD>The foreground color for the left pane of the applet.
 *         This will be the color of the text in the left pane.
 *         The default is white.  It must be a trio of integer
 *         values which make up a legal RGB color value.  Each
 *         integer value must be between 0 and 255, inclusively.
 *         The example sets the foreground color to a shade of red.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;leftforeground&quot;
 *                   VALUE=&quot;144, 32, 32&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>rightforeground</B></TD>
 *     <TD>The foreground color for the right pane of the applet.
 *         This will be the color of the text in the right pane.
 *         The default is white.  It must be a trio of integer
 *         values which make up a legal RGB color value.  Each
 *         integer value must be between 0 and 255, inclusively.
 *         The example sets the foreground color to a shade of red.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;rightforeground&quot;
 *                   VALUE=&quot;144, 32, 32&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>foreground</B></TD>
 *     <TD>The foreground color for the right pane of the applet.
 *         This will be the color of the text in the right pane.
 *         This is the same as rightforeground, and was left in
 *         for backwards compatibility.
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;foreground&quot;
 *                   VALUE=&quot;144, 32, 32&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>leftbackground</B></TD>
 *     <TD>The background color for the left pane of the applet.
 *         The default is black. It must be a trio of integer
 *         values which make up a legal RGB color value.  Each
 *         integer value must be between 0 and 255, inclusively.
 *         The example sets the background color to a light blue.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;leftbackground&quot;
 *                   VALUE=&quot;224, 224, 255&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>rightbackground</B></TD>
 *     <TD>The background color for the right pane of the applet.
 *         The default is black. It must be a trio of integer
 *         values which make up a legal RGB color value.  Each
 *         integer value must be between 0 and 255, inclusively.
 *         The example sets the background color to a light blue.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;rightbackground&quot;
 *                   VALUE=&quot;224, 224, 255&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>background</B></TD>
 *     <TD>The background color for the right pane of the applet.
 *         This is the same as rightbackground, and was left in
 *         for backwards compatibility.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;background&quot;
 *                   VALUE=&quot;224, 224, 255&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>refresh</B></TD>
 *     <TD>This indicates that the applet should re-request the data
 *         from the server after the text has been repeated
 *         <CODE>n</CODE> times, where n is the integer argument to
 *         this parameter.  The example to the right will reload the
 *         text data after it has completely displayed 3 times.  Note
 *         that this is simply when it <B>starts</B> to load the data
 *         again.  The data will most likely not be available
 *         instantaneously, so it will display the current data again
 *         before replacing it with the new data.  This means it might
 *         actually display 4 or even 5 times before you'll actually see
 *         the new data.  The benefit of this is that your data will not
 *         be changed in the middle of a presentation, it will only be
 *         updated at the end.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;refresh&quot; VALUE=&quot;3&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>fgloadcolor</B></TD>
 *     <TD>This lets you specify what the applet should use for a
 *         foreground color while it is loading data.  The default
 *         is blue.  This is the color the &quot;Loading data
 *         ...&quot; string will be displayed in.  You generally won't
 *         have to use this unless you are really concerned about it
 *         fitting into your page during loading.  The default is blue.
 *         It must be a trio of integer values which make up a legal RGB
 *         color value.  Each integer value must be between 0 and 255,
 *         inclusively.  The example sets the foreground color to green.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;fgloadcolor&quot; VALUE=&quot;32, 144, 32&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>bgloadcolor</B></TD>
 *     <TD>This lets you specify what the applet should use for a background
 *         color while it is loading data.  The default is light blue.
 *         It must be a trio of integer values which make up a legal RGB
 *         color value.  Each integer value must be between 0 and 255,
 *         inclusively.  The example sets the background color to a light
 *         green.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;background&quot; VALUE=&quot;224, 255, 224&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 *   <TR>
 *     <TD BGCOLOR="#E0E0FF"><B>wraptext</B></TD>
 *     <TD>This lets you specify whether the applet should apply
 *         automatic line wrapping or not.  An argument of
 *         <CODE>true</CODE> will apply line wrapping, an argument of
 *         <CODE>false</CODE> will wrap the lines as they appear in
 *         the data file.  This feature was included for users who
 *         want tight control of where lines are wrapped.
 *     </TD>
 *     <TD>
 *       <CODE>
 *         &lt;PARAM NAME=&quot;wraptext&quot; VALUE=&quot;false&quot;&gt;
 *       </CODE>
 *     </TD>
 *   </TR>
 * </TABLE>
 *
 *
 * <P>Note that most of the parameters can be specified in the text file
 * itself, with the exception of the name of the text file, and the text
 * wrapping.  The text wrapping is performed when the data is loaded.  Thus, 
 * the only mandatory <CODE>&lt;PARAM&gt;</CODE> tag is the <CODE>data</CODE>
 * tag.  Indeed, this is the only one I usually specify, since the data
 * file changes most of the other values several times during the
 * presentation.
 *
 * <P>If the user clicks on the scrolling text, the scrolling will stop.
 * If the user clicks again, it will start again, in the same place.
 * The exception is if a URL is set.  If a URL is set, then when the user
 * clicks on it, that URL will be loaded.
 *
 * <P>More complete documentation can be found at this applet's homepage
 * at <A HREF="http://kombat.org/Programming/TextScroll">
 * http://kombat.org/Programming/TextScroll</A>.
 *
 * @version 2.8.4 12 Jul 2004
 * @author Kevin Swan, kombat@kombat.org
 */

public class TextScroll extends Applet implements Runnable {

  /**
   * The current version of this applet.
   */
  public static final String VERSION = "2.8.4";

  /**
   * A constant representing the maximum number of milliseconds
   * the applet may sleep.  This is the maximum number a user may
   * specify as the speed value, and would represent a sleep time
   * of MIN_SPEED milliseconds.
   */
  public static final int MAX_SPEED = 100;

  /**
   * A constant representing the minimum number of milliseconds
   * the applet may sleep.  This is the minimum number a user may
   * specify as the speed value, and would represent a sleep time
   * of MAX_SPEED milliseconds.
   */
  public static final int MIN_SPEED = 1;

  /**
   * A constant representing the default number of milliseconds
   * the applet should sleep.  This value is used for the speed
   * if the user specifies an invalid value, or none at all.
   */
  public static final int DEFAULT_SPEED = 70;

  /**
   * This is the color to use for a background while the data is
   * loading.  Default is light blue.
   */
  public static final int BG_LOAD_COLOR = (224 << 16) | (224 << 8) | 255;

  /**
   * This is the color to draw the information in while the data is
   * loading.  Default is dark blue.
   */
  public static final int FG_LOAD_COLOR = (32 << 16) | (32 << 8) | 144;

  /**
   * This is the color to use for a background if an error occurs.
   * Default is light red.
   */
  public static final int BG_ERROR_COLOR = (255 << 16) | (224 << 8) | 224;

  /**
   * This is the color to draw the information in if an error occurs.
   * Default is dark red.
   */
  public static final int FG_ERROR_COLOR = (144 << 16) | (32 << 8) | 32;

  /**
   * A constant indicating the text data is ready.
   */
  public static final int T_READY = 1;

  /**
   * A constant used to indicate the text is loading.
   */
  public static final int T_LOADING = 2;

  /**
   * A constant used to indicate the data is being formatted.
   */
  public static final int T_FORMATTING = 4;

  /**
   * A constant used to indicate an error occured in loading the text.
   */
  public static final int T_NO_DATA_SPECIFIED = 8;

  /**
   * A constant used to indicate an error occured in loading the text.
   */
  public static final int T_LOAD_ERROR = 16;

  /** This is the String displayed when no URL is set. */
  public static final String STATUS_MSG = "TextScroll -- Version " +
                                          TextScroll.VERSION;

  /** The default Font face to use (&quot;SansSerif.&quot;) */
  public static final String DEFAULT_FONT_FACE = "SansSerif";

  /** The default Font size to use (12). */
  public static final int DEFAULT_FONT_SIZE = 12;

  /** The default width of the left panel (0). */
  public static final int DEFAULT_LEFT_WIDTH = 0;



  /** The Image to create and use for buffering. */
  private Image buffImage;

  /** The Graphics object from the buffer Image that we can draw on. */
  private Graphics buffGraphics;

  /** The height and width of the buffer canvas. */
  private int buffHeight, buffWidth;

  /** The Colors to use. */
  private Color leftBackgroundColor,
                leftForegroundColor,
                rightBackgroundColor,
                rightForegroundColor,
                bgLoadColor,
                fgLoadColor;

  /** The initial font.  This is used for refreshing the data. */
  private Font initialFont;

  /** The Font to use in the left pane. */
  private Font leftFont;

  /** The Font to use in the right pane. */
  private Font rightFont;

  /** The size of the Font to use in the left pane. */
  private int leftFontSize;

  /** The size of the Font to use in the right pane. */
  private int rightFontSize;

  /** 
   * The height in pixels of the font we're using.  This is important
   * when determining where to draw the next line of text.
   */
  private int fontHeight;

  /** The name of the file to load. */
  private String fileName;

  /** The URL of the data file to load. */
  private URL dataFileURL;

  /** The height and width of the applet/clipping window. */
  private int width, height;

  /** The width of the left panel. */
  private int leftWidth = TextScroll.DEFAULT_LEFT_WIDTH;

  /** Whether we should center the text in the left pane or not. */
  private boolean leftCenter = false;

  /** Whether we should center the text in the right pane or not. */
  private boolean rightCenter = false;

  /**
   * The speed to scroll at.  Can be from MIN_SPEED to MAX_SPEED.
   * Default is DEFAULT_SPEED.
   */
  private int speed;

  /**
   * This is the target URL the applet should load if the user
   * clicks on the applet.
   */
  private URL targetURL = null;

  /**
   * This flag indicates whether or not the apply should apply
   * automatic line wrapping to the data.  Default is to apply
   * line wrapping.
   */
  private boolean shouldWrapText = true;

  /**
   * This is the frame to display the contents of the target URL in
   * when the applet is clicked on.  If this is null, then the content
   * will be displayed in the current frame.
   */
  private String targetFrame = null;

  /** The x coordinate to start drawing text at. */
  private int inset = 3;

  /** boolean to flag if the Applet is currently running. */
  private boolean running;

  /**
   * boolean to flag whether or not we should display
   * the message in the left panel this time.  This is
   * set to true when the setLeftText directive is
   * invoked, and reset to false when the painting
   * routine actually gets around to displaying the
   * text.
   */
  private boolean showLeftText = false;

  /** The text to display in the left panel. */
  private String leftText = "";

  /**
   * This is how many pixels to allow between lines of text.
   */
  private int lineSpacing = 5;

  /** The actual text to display, one line per entry. */
  private String[] text;

  /** The "window" we're displaying in, used for scrolling. */
  private int frame = 16;

  /** The current line to append when we have to add another line. */
  private int currLine = 0;

  /**
   * Keeps a count of how many times we've displayed the data
   * completely.
   */
  private int iteration = 0;

  /**
   * This value specifies how many times to display the data before
   * refreshing from the server.  Values <= 0 indicate not to refresh
   * at all.
   */
  private int refreshValue = 1;

  /** Thread to control the scrolling. */
  private Thread scroller;

  /**
   * This is an undocumented element to allow greater control over
   * the positioning of the animation.  This is how many pixels to
   * move the text when scrolling.  The default is 1.
   */
  private int offset = 1;

  /**
   * This variable remembers whether the mouse is inside the applet or
   * not.  This is important because if it *is*, and we had a URL set,
   * then that URL is displayed in the status bar.  If the mouse stays
   * inside the applet, and the target URL changes, that should be
   * reflected in the status bar.  So, when setURL() is called, it must
   * check this flag to see if it should update the status bar.
   */
  private boolean mouseInside = false;

  /**
   * This flag monitors whether all the necessary data is here yet or not.
   */
  private int status;

  /**
   * The Object responsible for retrieving the text data.
   */
  private DataLoader loader = null;

  /**
   * This flag lets us know that we've tried to refresh the data.
   */
  private boolean reloaded = true;

  /**
   * The Object responsible for managing directives.
   */
  private DirectiveManager directiveManager = null;



  /**
   * Called during initialization.  This is where we check all run-time
   * flags set by the <CODE>&lt;PARAM&gt;</CODE> tags, and load the
   * text.  We will also prepare the text buffer.
   */
  public void init () {

    this.status = TextScroll.T_LOADING;
    this.text = null;
    this.directiveManager = new DirectiveManager (this);

    /* Print version info. */
    System.err.println ("TextScroll  v" + TextScroll.VERSION +
         "\nCopyright (C) 1998, 2004   Kevin Swan, kombat@kombat.org");

    running = false;

    String param = null;

    /*
     * Get the dimensions of the applet.  The buffer image will be exactly
     * as wide as the applet, but will be 200 pixels taller, supporting
     * a maximum font size of 200.
     */
    this.width  = this.size ().width;
    this.height = this.size ().height;
    this.buffWidth = this.width;
    this.buffHeight = this.height + 200;

    /*
     * Get the width of the left text area.
     */
    param = getParameter ("leftwidth");
    if (param == null)
      this.leftWidth = TextScroll.DEFAULT_LEFT_WIDTH;
    else
      try {
        this.leftWidth = Integer.parseInt (param);
      } catch (NumberFormatException nfe) {
        this.leftWidth = TextScroll.DEFAULT_LEFT_WIDTH;
      }

    /*
     * Get the color parameters for the text areas.
     */
    param = getParameter ("leftforeground");
    if (param == null)
      leftForegroundColor = Color.black;
    else
      leftForegroundColor = getColorFromString (param);
    if (leftForegroundColor == null)
      leftForegroundColor = Color.black;

    param = getParameter ("leftbackground");
    if (param == null)
      leftBackgroundColor = Color.white;
    else
      leftBackgroundColor = getColorFromString (param);
    if (leftBackgroundColor == null)
      leftBackgroundColor = Color.white;

    param = getParameter ("rightforeground");
    if (param == null)
      rightForegroundColor = Color.black;
    else
      rightForegroundColor = getColorFromString (param);
    if (rightForegroundColor == null)
      rightForegroundColor = Color.black;

    param = getParameter ("rightbackground");
    if (param == null)
      rightBackgroundColor = Color.white;
    else
      rightBackgroundColor = getColorFromString (param);
    if (rightBackgroundColor == null)
      rightBackgroundColor = Color.white;

    param = getParameter ("foreground");
    if (param == null)
      rightForegroundColor = Color.black;
    else
      rightForegroundColor = getColorFromString (param);
    if (rightForegroundColor == null)
      rightForegroundColor = Color.black;

    param = getParameter ("background");
    if (param == null)
      rightBackgroundColor = Color.white;
    else
      rightBackgroundColor = getColorFromString (param);
    if (rightBackgroundColor == null)
      rightBackgroundColor = Color.white;

    param = getParameter ("fgloadcolor");
    if (param == null)
      fgLoadColor = new Color (TextScroll.FG_LOAD_COLOR);
    else
      fgLoadColor = getColorFromString (param);
    if (fgLoadColor == null)
      fgLoadColor = new Color (TextScroll.FG_LOAD_COLOR);

    param = getParameter ("bgloadcolor");
    if (param == null)
      bgLoadColor = new Color (TextScroll.BG_LOAD_COLOR);
    else
      bgLoadColor = getColorFromString (param);
    if (bgLoadColor == null)
      bgLoadColor = new Color (TextScroll.BG_LOAD_COLOR);


    /*
     * Get the desired Font information.
     */
    param = getParameter ("leftfontsize");
    if (param == null)
      this.leftFontSize = TextScroll.DEFAULT_FONT_SIZE;
    else
      try {
        this.leftFontSize = Integer.parseInt (param);
      } catch (NumberFormatException nfe) {
        this.leftFontSize = TextScroll.DEFAULT_FONT_SIZE;
      }

    param = getParameter ("rightfontsize");
    if (param == null)
      this.rightFontSize = TextScroll.DEFAULT_FONT_SIZE;
    else
      try {
        this.rightFontSize = Integer.parseInt (param);
      } catch (NumberFormatException nfe) {
        this.rightFontSize = TextScroll.DEFAULT_FONT_SIZE;
      }

    param = getParameter ("fontsize");
    if (param == null)
      this.rightFontSize = TextScroll.DEFAULT_FONT_SIZE;
    else
      try {
        this.rightFontSize = Integer.parseInt (param);
      } catch (NumberFormatException nfe) {
        this.rightFontSize = TextScroll.DEFAULT_FONT_SIZE;
      }

    String face = null;

    param = getParameter ("leftfontface");
    if (param == null)
      face = TextScroll.DEFAULT_FONT_FACE;
    else
      face = param;

    this.leftFont = new Font (face, Font.PLAIN, this.leftFontSize);

    param = getParameter ("rightfontface");
    if (param == null)
      face = TextScroll.DEFAULT_FONT_FACE;
    else
      face = param;

    this.rightFont = new Font (face, Font.PLAIN, this.rightFontSize);
    this.initialFont = new Font (face, Font.PLAIN, this.rightFontSize);
    FontMetrics fm = this.getToolkit ().getFontMetrics (this.rightFont);
    this.fontHeight =
          fm.getMaxAscent () + fm.getMaxDescent () + fm.getLeading ();
    frame = this.lineSpacing + this.fontHeight;

    param = getParameter ("fontface");
    if (param == null)
      face = TextScroll.DEFAULT_FONT_FACE;
    else
      face = param;

    this.rightFont = new Font (face, Font.PLAIN, this.rightFontSize);
    this.initialFont = new Font (face, Font.PLAIN, this.rightFontSize);
    fm = this.getToolkit ().getFontMetrics (this.rightFont);
    this.fontHeight =
          fm.getMaxAscent () + fm.getMaxDescent () + fm.getLeading ();
    frame = this.lineSpacing + this.fontHeight;

    /*
     * Set the name of the file to load.
     */
    param = getParameter ("data");
    this.dataFileURL = null;
    if (param == null)
      this.status = TextScroll.T_NO_DATA_SPECIFIED;
    else {
      try {
        if (!param.startsWith ("http://"))
          this.dataFileURL = new URL (getDocumentBase (), param);
        else
          this.dataFileURL = new URL (param);
      } catch (MalformedURLException mue) {
        this.status = TextScroll.T_LOAD_ERROR;
      }
    }

    /*
     * Check if the user wants line wrapping or not.
     */
    param = getParameter ("wraptext");
    if (param != null)
      this.shouldWrapText = (new Boolean (param)).booleanValue ();

    /* Look for a different offset. */
    param = getParameter ("offset");
    if (param == null)
      this.offset = 1;
    else
      try {
        this.offset = Integer.parseInt (param);
      } catch (NumberFormatException  nfe) {
        this.offset = 1;
      }

    /* Set the speed information. */
    param = getParameter ("speed");
    if (param == null)
      this.speed = TextScroll.DEFAULT_SPEED;
    else
      try {
        this.speed = Integer.parseInt (param);
      } catch (NumberFormatException nfe) {
        this.speed = TextScroll.DEFAULT_SPEED;
      }

    if (this.speed > TextScroll.MAX_SPEED ||
        this.speed < TextScroll.MIN_SPEED)
      this.speed = TextScroll.DEFAULT_SPEED;

    /* Set the refresh information. */
    param = getParameter ("refresh");
    if (param == null)
      this.refreshValue = -1;
    else
      try {
        this.refreshValue = Integer.parseInt (param);
      } catch (NumberFormatException nfe) {
        this.refreshValue = -1;
      }

    this.resize (this.width, this.height);

    /*
     * buffGraphics is what we will use to draw on our buffImage, the
     * offscreen canvas we will use to write the text to before clipping
     * it and copying it to the active Graphics object in paint ().
     * We will support a maximum font size of 200.
     */
    this.buffImage = this.createImage (this.buffWidth, this.buffHeight);
    this.buffGraphics = this.buffImage.getGraphics ();

    /* Prepare the offscreen buffer. */

    this.buffGraphics.setColor (this.leftBackgroundColor);
    this.buffGraphics.fillRect (0, 0, this.leftWidth, this.buffHeight);

    this.buffGraphics.setColor (this.rightBackgroundColor);
    this.buffGraphics.fillRect (
               this.leftWidth, 0, this.buffWidth, this.buffHeight);

//    this.buffGraphics.setColor (this.rightForegroundColor);
//    this.buffGraphics.setFont (this.rightFont);

    loadData (this.dataFileURL);

  } /* init () */



  /**
   * buffGraphics is what we will use to draw on our buffImage, the
   * offscreen canvas we will use to write the text to before clipping
   * it and copying it to the active Graphics object in paint ().
   * We will support a maximum font size of 200.
   *
   * @return The <CODE>Graphics</CODE> object we will draw
   *    on for a buffer.
   */
  private Graphics getBuffGraphics () {
    if (this.buffGraphics == null) {
      this.buffImage = this.createImage (this.buffWidth, this.buffHeight);
      this.buffGraphics = this.buffImage.getGraphics ();
    }
    return this.buffGraphics;
  }



  /**
   * This method attempts to convert a <CODE>String</CODE> numeric
   * representation of a color into an actual <CODE>Color</CODE> object.
   * It expects the given <CODE>String</CODE> to be in the format
   * &quot;<CODE>red,green,blue</CODE>&quot;
   *
   * @param rgb A comma-separated RGB numeric representation of a
   *            color, passed as a <CODE>String</CODE>.
   *
   * @return A <CODE>Color</CODE> object represented by the given RGB value,
   *         if the values are legal.  If an error occurs, return
   *         <CODE>null</CODE>.
   */
  public static Color getColorFromString (String rgb) {

    int red, green, blue;

    red = green = blue = 0;

    if (rgb == null)
      return null;

    try{
      red   = Integer.parseInt ((rgb.substring (0, rgb.indexOf (","))).trim ());
      green = Integer.parseInt ((rgb.substring(rgb.indexOf (",") + 1, rgb.lastIndexOf (","))).trim ());
      blue  = Integer.parseInt ((rgb.substring(rgb.lastIndexOf (",") + 1)).trim ());
    } catch (NumberFormatException nfe) {
      return null;
    }

    try {
      return new Color(red, green, blue);
    } catch (IllegalArgumentException iae) {
      return null;
    }
  } /* getColorFromString () */



  /**
   * The implementation of the <CODE>run ()</CODE> method, as required by
   * implementing the <CODE>Runnable</CODE> interface.  This method performs
   * the actual animation.
   */
  public void run () {

    while (!this.isReady ()) {
      if (this.getStatus () > TextScroll.T_LOADING) {
        repaint ();
        return;
      }
      try {
        scroller.sleep (200);
      } catch (InterruptedException ie) {
        return;
      }
    }

    while (running) {

      /* Move the area upwards offset pixels. */
      this.getBuffGraphics ().copyArea (
                  0,
                  offset,
                  this.buffWidth,
                  this.height + this.fontHeight + this.lineSpacing,
                  0, 0 - offset);
      frame -= offset;

      /* The frame tells us when its time to draw another line of text. */
      if (frame < 0) {
        /* Get the next line in the source file. */
        String line = this.text [this.currLine];
        this.currLine++;

        /*
         * If we've hit the end of the presentation, reset the line
         * variable, increment the iteration variable, and test if
         * we should refresh our data.
         */
        if (this.currLine == this.text.length) {
          this.currLine = 0;
          this.iteration++;

          /* See if we should refresh our data. */
          if ((this.refreshValue > 0) &&
              (this.iteration >= this.refreshValue)) {
            /* 
             * First, we should check to see if we've already tried to refresh
             * the data.  If we have, and its ready now, replace the data.
             */
            if (this.reloaded) {
              if (this.isReady ()) {
                String[] tmpArr = this.loader.getData ();
                if (this.loader.errorOccurred ()) {
	                this.loader = null;
	                System.err.println (
                    " Error reading data file, creating new dataloader ***");
	                this.loadData (this.dataFileURL);
	              } else {
                  this.text = new String [tmpArr.length];
                  System.arraycopy (tmpArr, 0, this.text, 0, tmpArr.length);
                  this.iteration = 0;
                  this.reloaded = false;
                }
              }
            } else
              /* If we haven't started refreshing it yet, do it now. */
              this.refreshData ();
          }
        }

        /*
         * If the line is a method directive, try to service it.
         */
        if (line.startsWith ("^^"))
          if (!this.directiveManager.performDirective (line))
            System.err.println ("Illegal directive call:\n\t" + line);
          else
            continue;

        /* Treat the line as normal. */

        /**
         * If we have a left message set, and we haven't
         * displayed it yet, now is the time to display it.
         */

        FontMetrics fm = this.getToolkit ().getFontMetrics (this.leftFont);
        if (this.showLeftText) {
          this.getBuffGraphics ().setColor (this.leftForegroundColor);
          this.getBuffGraphics ().setFont (this.leftFont);
          if (!this.leftCenter) {
            this.getBuffGraphics ().drawString (
                    leftText,
                    0,
                    this.height + this.lineSpacing + fm.getMaxAscent ());
          } else {
            /*
             * We must center the text.  We will do this in 3 steps:
             *   1. Find the length of the text.
             *   2. Calculate what x value to start drawing the text at.
             *   3. Draw the text.
             */
            int length = fm.stringWidth (leftText);
            int start = (this.leftWidth / 2) - (length / 2);
            start = (start < 0) ? 0 : start;
            this.getBuffGraphics ().drawString (
                    leftText,
                    start,
                    this.height + this.lineSpacing + fm.getMaxAscent ());
          }

          this.getBuffGraphics ().setColor (this.rightForegroundColor);
          this.showLeftText = false;
        }

        this.getBuffGraphics ().setColor (this.rightForegroundColor);
        this.getBuffGraphics ().setFont (this.rightFont);
        fm = this.getToolkit ().getFontMetrics (this.rightFont);
        frame = this.lineSpacing + this.fontHeight;
        if (!this.rightCenter) {
          this.getBuffGraphics ().drawString (
                  line,
                  this.leftWidth + this.inset,
                  this.height + this.lineSpacing + fm.getMaxAscent ());
        } else {
          /*
           * We must center the text.  We will do this in 3 steps:
           *   1. Find the length of the text.
           *   2. Calculate what x value to start drawing the text at.
           *   3. Draw the text.
           */
          int length = fm.stringWidth (line);
          int start = ((this.width - this.leftWidth) / 2) - (length / 2);
          start = (start < this.leftWidth) ? this.leftWidth : start;
          this.getBuffGraphics ().drawString (
                  line,
                  start + this.leftWidth,
                  this.height + this.lineSpacing + fm.getMaxAscent ());
        }
      }

      try {
        scroller.sleep (TextScroll.MAX_SPEED + 1 - this.speed);
      } catch (InterruptedException ie) {
        return;
      }

      this.repaint ();

    }

    return;

  } /* run () */



  /**
   * This method reloads the text data file from the
   * server.  Note that by the time this method is
   * called, it will probably take some time to
   * completely load, and we can't really replace the
   * whole data array in the middle of a display.  We'll
   * simply refresh the <CODE>DataLoader</CODE>.  The
   * next time we get to the end of the presentation,
   * we'll check if the data is ready.  But, we do have
   * to note somehow that we tried to refresh it.  We'll
   * set a flag.
   */
  private void refreshData () {
    this.loader.refresh ();
    this.reloaded = true;
  }



  /**
   * This method loads data from a <CODE>URL</CODE> into
   * a <CODE>DataLoader</CODE>.  The applet should
   * periodically check its <CODE>DataLoader</CODE> to see
   * if the data is ready.  It is given the URL of the text
   * file to read.
   *
   * @param url The URL of the text data file to load.
   */
  private void loadData (URL url) {
    /* If it is null, just ignore it. */
    if (url == null)
      return;
    else
      this.loader = new DataLoader (
                      url,
                      this.initialFont,
                      this.width - this.leftWidth,
                      this.shouldWrapText);
    return;
  }



  /**
   * Called to paint the screen.
   */
  public void paint (Graphics g) {
    if (this.status > TextScroll.T_READY)
      this.displayStatus (g);
    else
      g.drawImage (this.buffImage, 0, 0, this);
  }



  /**
   * Called to update the screen.
   */
  public void update (Graphics g) {
    if (this.status > TextScroll.T_READY)
      this.displayStatus (g);
    else
      g.drawImage (this.buffImage, 0, 0, this);
  } 



  /**
   * Called to print the appropriate status information if the applet
   * is not able to begin scrolling yet.
   *
   * @param g The <CODE>Graphics</CODE> object to draw on.
   */
  private void displayStatus (Graphics g) {
    if (this.status == TextScroll.T_READY)
      return;

    g.setFont (new Font ("SansSerif", Font.PLAIN, 12));
    switch (this.getStatus ()) {
      case (TextScroll.T_LOADING):
                g.setColor (this.bgLoadColor);
                g.fillRect (0, 0, this.width, this.height);
                g.setColor (this.fgLoadColor);
                g.drawString ("Loading data ...", 5, 15);
                return;
      case (TextScroll.T_FORMATTING):
                g.setColor (this.bgLoadColor);
                g.fillRect (0, 0, this.width, this.height);
                g.setColor (this.fgLoadColor);
                g.drawString ("Formatting data ...", 5, 15);
                return;
      case (TextScroll.T_NO_DATA_SPECIFIED):
                g.setColor (new Color (TextScroll.BG_ERROR_COLOR));
                g.fillRect (0, 0, this.width, this.height);
                g.setColor (new Color (TextScroll.FG_ERROR_COLOR));
                g.drawString ("No \"data\" parameter specified.", 5, 15);
                return;
      case (TextScroll.T_LOAD_ERROR):
                g.setColor (new Color (TextScroll.BG_ERROR_COLOR));
                g.fillRect (0, 0, this.width, this.height);
                g.setColor (new Color (TextScroll.FG_ERROR_COLOR));
                g.drawString ("Couldn't read specified text file.", 5, 15);
                return;
    }
  } /* displayStatus () */



  /**
   * Called to start this applet.  If the applet has been running
   * before, we will pick up where we left off.
   */
  public void start () {
    running = true;
    (scroller = new Thread (this)).start ();
    if (this.loader != null)
      this.loader.start ();
    else
      System.err.println ("ERROR: DataLoader reference was lost.");
  }



  /**
   * Stop the applet.
   */
  public void stop () {
    running = false;
    if (this.scroller != null)
      this.scroller.stop ();
    if (this.loader != null)
      this.loader.stop ();
  }


  /**
   * If the Applet is running, stop it.  If it is stopped, restart it.
   */
  private void toggle () {
    if (this.running)
      this.stop ();
    else
      this.start ();
  }



  /**
   * Method called when the mouse pointer enters the applet.
   *
   * @param e The actual event
   * @param x The x coordinate the event occurred at.
   * @param y The y coordinate the event occurred at.
   *
   * @return true if the event is handled, false otherwise.
   */
  public boolean mouseEnter (Event e, int x, int y) {
    this.mouseInside = true;
    if (this.getURL () == null)
      this.showStatus (TextScroll.STATUS_MSG);
    else
      this.showStatus (this.getURL ().toString ());
    return true;
  }


  /**
   * Method called when the mouse pointer leaves the applet.
   *
   * @param e The actual event
   * @param x The x coordinate the event occurred at.
   * @param y The y coordinate the event occurred at.
   *
   * @return true if the event is handled, false otherwise.
   */
  public boolean mouseExit (Event e, int x, int y) {
    this.mouseInside = false;
    this.showStatus ("");
    return true;
  }


  /**
   * Method called when the user clicks.
   *
   * @param e The actual event.
   * @param x The x coordinate the even occurred at.
   * @param y The y coordinate the even occurred at.
   *
   * @return true if the event is handled, false otherwise.
   */
  public boolean mouseDown (Event me, int x, int y) {
    if (this.getURL () == null)
      this.toggle ();
    else
      try {
        if (this.getAppletContext () != null) {
          if (this.targetFrame == null)
            this.getAppletContext ().showDocument (this.getURL ());
          else
            this.getAppletContext ().showDocument (this.getURL (),
                                                   this.targetFrame);
        }
      } catch (Exception e) {
        this.toggle ();
      }
    return true;
  }



  /**
   * This method is used to determine whether the data is ready or not.
   *
   * @return An <CODE>int</CODE> value which is one of the constants:
   *           <UL>
   *             <LI><CODE>T_READY</CODE> - If the text is ready.
   *             <LI><CODE>T_LOADING</CODE> - If the text is loading.
   *             <LI><CODE>T_NO_DATA_SPECIFIED</CODE> - If the text could
   *                 not be loaded because no &quot;data&quot; value was
   *                 specified in a <CODE>&lt;PARAM&gt;</CODE> tag.
   *             <LI><CODE>T_LOAD_ERROR</CODE> - If an error occurred
   *                 during loading of the text data, probably a file
   *                 not found or a permissions problem.
   *           </UL>
   */
  public int getStatus () {
    return this.status;
  }



  /**
   * A method used to determine whether we're ready to start the animation
   * or not.
   *
   * @return <CODE>true</CODE> if all the necessary data has been loaded,
   *         <CODE>false</CODE> otherwise.
   */
  public boolean isReady () {
    if (this.text == null) {
      if (this.loader.dataReady ()) {
        String[] tmpArr = this.loader.getData ();
        this.text = new String [tmpArr.length];
        System.arraycopy (tmpArr, 0, this.text, 0, tmpArr.length);
        this.status = TextScroll.T_READY;
      } else {
        if (this.loader.errorOccurred ())
          this.status = TextScroll.T_LOAD_ERROR;
        else
          this.status = TextScroll.T_LOADING;
      }
    }

    return ((this.status & TextScroll.T_READY) == TextScroll.T_READY);
  }



  /**
   * This method is used to specify whether the text in the
   * left pane should be centered or not.  The String argument
   * must either be &quot;true&quot; or &quot;false&quot;  The
   * comparison is case-insensitive.
   *
   * @param boolStr A String.  Must be &quot;true&quot; to specify
   *                that all further text in the left pane should
   *                be centered, or &quot;false&quot; to specify
   *                normal formatting (left align).  Any other
   *                values are ignored, and a quiet message is
   *                printed to stderr.
   */
  public void setLeftCenter (String boolStr) {
    if (boolStr.equalsIgnoreCase ("true"))
      this.leftCenter = true;
    else if (boolStr.equalsIgnoreCase ("false"))
      this.leftCenter = false;
    else
      System.err.println (
           "setLeftCenter directive ignored - invalid argument: " +
           boolStr);
  }



  /**
   * This method is used to specify whether the text in the
   * right pane should be centered or not.  The String argument
   * must either be &quot;true&quot; or &quot;false&quot;  The
   * comparison is case-insensitive.
   *
   * @param boolStr A String.  Must be &quot;true&quot; to specify
   *                that all further text in the right pane should
   *                be centered, or &quot;false&quot; to specify
   *                normal formatting (left align).  Any other
   *                values are ignored, and a quiet message is
   *                printed to stderr.
   */
  public void setRightCenter (String boolStr) {
    if (boolStr.equalsIgnoreCase ("true"))
      this.rightCenter = true;
    else if (boolStr.equalsIgnoreCase ("false"))
      this.rightCenter = false;
    else
      System.err.println (
           "setRightCenter directive ignored - invalid argument: " +
           boolStr);
  }



  /**
   * This method is used to specify whether the text in the
   * right pane should be centered or not.  The String argument
   * must either be &quot;true&quot; or &quot;false&quot;  The
   * comparison is case-insensitive.  This method is left in
   * for compatibility reasons.
   *
   * @param boolStr A String.  Must be &quot;true&quot; to specify
   *                that all further text in the right pane should
   *                be centered, or &quot;false&quot; to specify
   *                normal formatting (left align).  Any other
   *                values are ignored, and a quiet message is
   *                printed to stderr.
   */
  public void setCenter (String boolStr) {
    if (boolStr.equalsIgnoreCase ("true"))
      this.rightCenter = true;
    else if (boolStr.equalsIgnoreCase ("false"))
      this.rightCenter = false;
    else
      System.err.println (
           "setCenter directive ignored - invalid argument: " +
           boolStr);
  }



  /**
   * This method is used to turn bold font styling for the
   * left pane on or off.  It expects a single String argument
   * which must be either &quot;true&quot; or &quot;false&quot;.
   * The comparison is case-insensitive.
   *
   * @param boolStr A String.  Must be &quot;true&quot; to specify
   *                that all further text displayed in the left
   *                pane should be bold, or &quot;false&quot; to
   *                specify non-bold styling.  Any other values are
   *                ignored, and a quiet message is printed to stderr.
   */
  public void setLeftBold (String boolStr) {
    int style = this.leftFont.getStyle ();
    if (boolStr.equalsIgnoreCase ("true"))
      style = style | Font.BOLD;
    else if (boolStr.equalsIgnoreCase ("false"))
      style = style & ~Font.BOLD;
    else {
      System.err.println ("setLeftBold directive ignored - invalid argument: " +
               boolStr);
      return;
    }
    this.leftFont =
           new Font (this.leftFont.getName (), style, this.leftFontSize);
  }



  /**
   * This method is used to turn bold font styling for the
   * right pane on or off.  It expects a single String argument
   * which must be either &quot;true&quot; or &quot;false&quot;.
   * The comparison is case-insensitive.
   *
   * @param boolStr A String.  Must be &quot;true&quot; to specify
   *                that all further text displayed in the right
   *                pane should be bold, or &quot;false&quot; to
   *                specify non-bold styling.  Any other values are
   *                ignored, and a quiet message is printed to stderr.
   */
  public void setRightBold (String boolStr) {
    int style = this.rightFont.getStyle ();
    if (boolStr.equalsIgnoreCase ("true"))
      style = style | Font.BOLD;
    else if (boolStr.equalsIgnoreCase ("false"))
      style = style & ~Font.BOLD;
    else {
      System.err.println (
          "Error: Invalid int arg for setBold or setRightBold: " +
          boolStr);
      return;
    }
    this.rightFont =
        new Font (this.rightFont.getName (), style, this.rightFontSize);
    FontMetrics fm = this.getToolkit ().getFontMetrics (this.rightFont);
    this.fontHeight =
        fm.getMaxAscent () + fm.getMaxDescent () + fm.getLeading ();
    this.getBuffGraphics ().setFont (this.rightFont);
  }



  /**
   * This method is used to turn bold font styling for the
   * right pane on or off.  It expects a single String argument
   * which must be either &quot;true&quot; or &quot;false&quot;.
   * The comparison is case-insensitive.  This method is left
   * in for compatibility purposes.
   *
   * @param boolStr A String.  Must be &quot;true&quot; to specify
   *                that all further text displayed in the right
   *                pane should be bold, or &quot;false&quot; to
   *                specify non-bold styling.  Any other values are
   *                ignored, and a quiet message is printed to stderr.
   */
  public void setBold (String boolStr) {
    this.setRightBold (boolStr);
  }



  /**
   * This method is used to turn italic font styling for the
   * left pane on or off.  It expects a single String argument
   * which must be either &quot;true&quot; or &quot;false&quot;.
   * The comparison is case-insensitive.
   *
   * @param boolStr A String.  Must be &quot;true&quot; to specify
   *                that all further text displayed in the left
   *                pane should be italic, or &quot;false&quot; to
   *                specify non-italic styling.  Any other values
   *                are ignored, and a quiet message is printed to
   *                stderr.
   */
  public void setLeftItalic (String boolStr) {
    int style = this.leftFont.getStyle ();
    if (boolStr.equalsIgnoreCase ("true"))
      style = style | Font.ITALIC;
    else if (boolStr.equalsIgnoreCase ("false"))
      style = style & ~Font.ITALIC;
    else {
      System.err.println (
          "Error: Invalid int arg for setLeftItalic: " +
          boolStr);
      return;
    }
    this.leftFont =
            new Font (this.leftFont.getName (), style, this.leftFontSize);
  }



  /**
   * This method is used to turn italic font styling for the
   * right pane on or off.  It expects a single String argument
   * which must be either &quot;true&quot; or &quot;false&quot;.
   * The comparison is case-insensitive.
   *
   * @param boolStr A String.  Must be &quot;true&quot; to specify
   *                that all further text displayed in the right
   *                pane should be italic, or &quot;false&quot; to
   *                specify non-italic styling.  Any other values
   *                are ignored, and a quiet message is printed to
   *                stderr.
   */
  public void setRightItalic (String boolStr) {
    int style = this.rightFont.getStyle ();
    if (boolStr.equalsIgnoreCase ("true"))
      style = style | Font.ITALIC;
    else if (boolStr.equalsIgnoreCase ("false"))
      style = style & ~Font.ITALIC;
    else {
      System.err.println (
          "Error: Invalid int arg for setItalic or setRightItalic: " +
          boolStr);
      return;
    }
    this.rightFont =
            new Font (this.rightFont.getName (), style, this.rightFontSize);
    FontMetrics fm = this.getToolkit ().getFontMetrics (this.rightFont);
    this.fontHeight =
          fm.getMaxAscent () + fm.getMaxDescent () + fm.getLeading ();
    this.getBuffGraphics ().setFont (this.rightFont);
  }



  /**
   * This method is used to turn italic font styling for the
   * right pane on or off.  It expects a single String argument
   * which must be either &quot;true&quot; or &quot;false&quot;.
   * The comparison is case-insensitive.  This method is left
   * in for compatibility purposes.
   *
   * @param boolStr A String.  Must be &quot;true&quot; to specify
   *                that all further text displayed in the right
   *                pane should be italic, or &quot;false&quot; to
   *                specify non-italic styling.  Any other values
   *                are ignored, and a quiet message is printed to
   *                stderr.
   */
  public void setItalic (String boolStr) {
    this.setRightItalic (boolStr);
  }




  /**
   * This method is used to set the inset value.  The inset is the
   * x coordinate where the text will be drawn at.  The default is 3.
   * The argument must be a String object which can be parsed to an
   * int.
   *
   * @param insetStr A String which can be parsed to an int.
   */
  public void setInset (String insetStr) {
    int val;

    try {
      val = Integer.parseInt (insetStr);
    } catch (NumberFormatException nfe) {
      System.err.println ("setInset directive ignored - invalid argument: " +
            insetStr + " (Expected a number)");
      return;
    }

    this.inset = val;

  }



  /**
   * This method sets the currently active URL to the given URL,
   * such that if the user clicks anywhere in the applet area
   * after this method has been invoked, it will load the named
   * URL, unless <CODE>URLString</CODE> is &quot;null&quot;
   * (case insensitive), in which case clicking will simply
   * result in toggling scrolling.
   *
   * <P>Note that in versions 2.7 and greater, the user can specify
   * a target frame for the URL to be displayed in as well.  The
   * frame name must be specified, followed by a comma, followed
   * by the target URL.
   *
   * <P>This method was modified in version 2.8.2 to be more
   * intelligent in determining if the user really wants a frame
   * or not.  Some URLs contain commas.  This method now only
   * treats it as a frame if there is a comma before the first
   * &quot;http://&quot; string.
   *
   * @param URLString The URL to load.  If the string is only a URL,
   *                  then the page will be loaded in the current
   *                  frame.  If it consists of a string, followed
   *                  by a comma, followed by another string, the
   *                  first string will be treated as the target
   *                  frame name, and the second will be treated
   *                  as the target URL.  If <CODE>URLString</CODE>
   *                  is &quot;null.&quot; (case insensitive),
   *                  then the target URL and the target frame are
   *                  both set to <CODE>null</CODE>.
   */
  public void setURL (String URLString) {

    /*
     * They might want a target frame, see if they've specified one.
     */
    if (URLString == null) {
        this.setURL ((URL) null);
        this.setTargetFrame (null);
    }

    /*
     * If the URL string contains a comma, and that comma appears
     * before the http://, then we have a target frame.  Otherwise,
     * just treat the whole string as a single URL.
     */
    if (URLString.indexOf (',') >= 0)
      if (URLString.indexOf (',') < URLString.indexOf ("http://")) {
        String targetFrame = URLString.substring (0, URLString.indexOf (','));
        String targetURL   = URLString.substring (URLString.indexOf (',') + 1,
                                                  URLString.length ());
        this.setURL (targetURL);
        this.setTargetFrame (targetFrame);
        return;
      }

    this.setTargetFrame (null);

    if (URLString.equalsIgnoreCase ("null")) {
      this.setURL ((URL) null);
      this.setTargetFrame (null);
    } else
      try {
        this.setURL (new URL (URLString));
      } catch (MalformedURLException mue) {
        System.err.println ("setURL directive ignored - invalid argument: " +
              URLString + "\n(Unsetting clickable link)");
        this.setURL ((URL) null);
      }
  }



  /**
   * This method sets the name of the target frame to display the
   * target URL in.  If <CODE>targetFrame</CODE> is null, then the
   * target URLs will be displayed in the current browser frame.
   *
   * @param targetFrame A <CODE>String</CODE> name of the target
   *                    frame to display the contents of the target
   *                    URL in when the applet is clicked on.
   */
  public void setTargetFrame (String targetFrame) {
    if (targetFrame != null) {
      targetFrame = targetFrame.trim ();
      if (targetFrame.equalsIgnoreCase ("null"))
        targetFrame = null;
    }
    this.targetFrame = targetFrame;
  }



  /**
   * An accessor method to get the current value of the target frame.
   *
   * @return The current target frame.
   */
  public String getTargetFrame () {
    return this.targetFrame;
  }



  /**
   * This method is used to set the target URL to an actual
   * <CODE>URL</CODE> object.  It is used by the mouse event
   * handler.
   *
   * @param url The <CODE>URL</CODE> to load in the page if
   *        the user clicks on the applet.
   */
  public void setURL (URL url) {
    this.targetURL = url;
    if (this.mouseInside)
      if (this.targetURL == null)
        this.showStatus (TextScroll.STATUS_MSG);
      else
        this.showStatus (this.targetURL.toString ());
  }



  /**
   * Display the given string in the left panel.
   *
   * @param msg The string to display in the left
   *    panel.
   */
  public void setLeftText (String msg) {
    this.leftText = msg;
    this.showLeftText = true;
  }



  /**
   * Answer the currently set target URL.
   *
   * @return The current URL target.
   */
  public URL getURL () {
    return this.targetURL;
  }



  /**
   * Answer a little blurb about this applet.
   *
   * @return info about this applet.
   */
  public String getAppletInfo () {
    return
      "TextScroll  Version " + VERSION +
      "  Copyright (C) 1998, 2004 by Kevin Swan, kombat@kombat.org";
  }



  /**
   * Answer information about legal parameters.
   *
   * @return info about the parameters.
   */
  public String[][] getParameterInfo () {
    String[][] pinfo = {
       { "fontface", "Serif, SansSerif, Monospaced", "The font to use" },
       { "fontsize", "integer", "The size of font to use" },
       { "speed", TextScroll.MIN_SPEED + " - " + TextScroll.MAX_SPEED, "Scroll speed" },
       { "data", "String", "Name of text file to display" },
       { "foreground", "rrr,ggg,bbb", "RGB value to use for foreground color" },
       { "background", "rrr,ggg,bbb", "RGB value to use for background color" }
    };
    return pinfo;
  }



  /**
   * Sets the left foreground color to the given RGB String.
   *
   * @param rgb a String representing a comma-seperated RGB value.
   */
  public void setLeftForegroundColor (String rgb) {
    Color color = getColorFromString (rgb);
    if (color != null)
      this.leftForegroundColor = color;
    this.getBuffGraphics ().setColor (this.leftForegroundColor);
  }



  /**
   * Sets the left background color to the given RGB String.
   *
   * @param rgb a String representing a comma-seperated RGB value.
   */
  public void setLeftBackgroundColor (String rgb) {
    Color color = getColorFromString (rgb);
    if (color != null)
      leftBackgroundColor = color;
    this.getBuffGraphics ().setColor (this.leftBackgroundColor);
    this.getBuffGraphics ().fillRect (0, 0, this.leftWidth, this.buffHeight);
    this.getBuffGraphics ().setColor (this.rightForegroundColor);
  }



  /**
   * Sets the right foreground color to the given RGB String.
   *
   * @param rgb a String representing a comma-seperated RGB value.
   */
  public void setRightForegroundColor (String rgb) {
    Color color = getColorFromString (rgb);
    if (color != null)
      this.rightForegroundColor = color;
    this.getBuffGraphics ().setColor (this.rightForegroundColor);
  }



  /**
   * Sets the right foreground color to the given RGB String.
   * This method is left in for backwards compatibility.
   *
   * @param rgb a String representing a comma-seperated RGB value.
   */
  public void setForegroundColor (String rgb) {
    this.setRightForegroundColor (rgb);
  }



  /**
   * Sets the right background color to the given RGB String.
   *
   * @param rgb a String representing a comma-seperated RGB value.
   */
  public void setRightBackgroundColor (String rgb) {
    Color color = getColorFromString (rgb);
    if (color != null)
      rightBackgroundColor = color;
    this.getBuffGraphics ().setColor (this.rightBackgroundColor);
    this.getBuffGraphics ().fillRect (this.leftWidth,
                                0,
                                this.buffWidth,
                                this.buffHeight);
    this.getBuffGraphics ().setColor (this.rightForegroundColor);
  }



  /**
   * Sets the right background color to the given RGB String.
   * This method is left in for backwards compatibility.
   *
   * @param rgb a String representing a comma-seperated RGB value.
   */
  public void setBackgroundColor (String rgb) {
    this.setRightBackgroundColor (rgb);
  }



  /**
   * Try and pause for the given number of milliseconds.  Note
   * that the time is given as a String.
   *
   * @param time a String that should be able to be converted to an
   *             Integer.
   */
  public void pause (String timeStr) {
    Integer time;
    try {
      time = Integer.valueOf (timeStr);
    } catch (NumberFormatException nfe) {
      System.err.println (
        "Error: Invalid integer specified for pause (): \"" +
        timeStr +
        "\"");
      return;
    }
    try {
      this.scroller.sleep (time.intValue ());
    } catch (InterruptedException ie) {
    }
    return;
  }



  /**
   * This is simply a way for the user to call <CODE>toggle ()</CODE>
   * as a directive.  This was done to relieve confusion, so the user
   * can simply use <CODE>pause ()</CODE> with or without an argument
   * to cause the applet to pause scrolling.  If it is called with
   * no arguments, this method is called, and the scrolling stops
   * until the user clicks the text area.
   *
   */
  public void pause () {
    this.toggle ();
  }



  /**
   * Sets the speed of the applet to the given value.  Note that the
   * speed value should be an integer in the form of a String, between
   * MIN_SPEED and MAX_SPEED.  If an invalid value is specified,
   * the speed value is left at its current setting.
   *
   * @param speedStr a String representing the integer speed to use for
   *        this applet.
   */
  public void setSpeed (String speedStr) {
    Integer speed;
    try {
      speed = Integer.valueOf (speedStr);
    } catch (NumberFormatException nfe) {
      System.err.println (
          "Error: Invalid integer specified for setSpeed (): \"" +
          speedStr +
          "\"");
      return;
    }

    if ((speed.intValue () <= TextScroll.MAX_SPEED) &&
        (speed.intValue () >= TextScroll.MIN_SPEED))
      this.speed = speed.intValue ();

    return;
  }



  /**
   * This method allows the user to change fonts in the middle of
   * the scrolling.  It sets the font to use for the left pane.
   *
   * @param face The <CODE>String</CODE> name of the new
   *             <CODE>Font</CODE> to use in the left pane.
   */
  public void setLeftFontFace (String face) {
    this.leftFont =
        new Font (face, this.leftFont.getStyle (), this.leftFontSize);
  }



  /**
   * This method allows the user to change fonts in the middle of
   * the scrolling.  It sets the font to use for the right pane.
   *
   * @param face The <CODE>String</CODE> name of the new
   *             <CODE>Font</CODE> to use in the right pane.
   */
  public void setRightFontFace (String face) {
    this.rightFont =
        new Font (face, this.rightFont.getStyle (), this.rightFontSize);
    FontMetrics fm = this.getToolkit ().getFontMetrics (this.rightFont);
    this.fontHeight =
        fm.getMaxAscent () + fm.getMaxDescent () + fm.getLeading ();
    this.getBuffGraphics ().setFont (this.rightFont);
  }



  /**
   * This method allows the user to change fonts in the middle of
   * the scrolling.  It sets the font to use for the right pane.
   * This method is left in for backwards compatibility.
   *
   * @param face The <CODE>String</CODE> name of the new
   *             <CODE>Font</CODE> to use in the right pane.
   */
  public void setFontFace (String face) {
    this.setRightFontFace (face);
  }



  /**
   * This method allows the user to change font size
   * in the middle of the scrolling.  It sets the
   * size of the font to use in the left pane.
   *
   * @param sizeStr The <CODE>String</CODE> representing
   *    an integer size of the <CODE>Font</CODE> to use
   *    in the left pane.
   */
  public void setLeftFontSize (String sizeStr) {
    int size;

    try {
      size = Integer.parseInt (sizeStr);
    } catch (NumberFormatException nfe) {
      System.err.println (
           "Error: Invalid integer specified for setLeftFontSize (): \"" +
           sizeStr +
           "\"");
      return;
    }

    this.leftFontSize = size;
    this.leftFont =
           new Font (this.leftFont.getName(),
                     this.leftFont.getStyle (),
                     this.leftFontSize);
  }



  /**
   * This method allows the user to change font size
   * in the middle of the scrolling.  It sets the
   * size of the font to use in the right pane.
   *
   * @param sizeStr The <CODE>String</CODE> representing
   *    an integer size of the <CODE>Font</CODE> to use
   *    in the right pane.
   */
  public void setRightFontSize (String sizeStr) {
    int size;

    try {
      size = Integer.parseInt (sizeStr);
    } catch (NumberFormatException nfe) {
      System.err.println (
           "Error: Invalid arg for setFontSize or setRightFontSize (): \"" +
           sizeStr +
           "\"");
      return;
    }

    this.rightFontSize = size;
    this.rightFont =
           new Font (this.rightFont.getName(),
                     this.rightFont.getStyle (),
                     this.rightFontSize);
    FontMetrics fm = this.getToolkit ().getFontMetrics (this.rightFont);
    this.fontHeight =
        fm.getMaxAscent () + fm.getMaxDescent () + fm.getLeading ();
    this.getBuffGraphics ().setFont (this.rightFont);
  }



  /**
   * This method allows the user to change font size
   * in the middle of the scrolling.  It sets the
   * size of the font to use in the right pane.
   * It is left in for backwards compatibility.
   *
   * @param sizeStr The <CODE>String</CODE> representing
   *    an integer size of the <CODE>Font</CODE> to use
   *    in the right pane.
   */
  public void setFontSize (String sizeStr) {
    this.setRightFontSize (sizeStr);
  }






}

