ORIGINAL DRAFT

This month, we’ll build a pair of high-level widgets for application development. JSplash is a simple splash screen window that displays a centered image for a timeout period, until the user presses a key or clicks the mouse. JTips automatically presents the user with a cycling set of tips at application startup. The tips are loaded from a simple text file and JTips provides options so the user can either disable it or move through the list interactively.

The JSplash Window

Listing 1 shows the code required to present a splash screen image to the user. The JSplash window is a fairly simple object, but it has to handle a number of circumstances correctly. Here’s a short list of requirements:

  • If the user does nothing, we should timeout after a given period.
  • If the user presses a key or clicks the mouse, we dismiss the window.
  • Startup conditions should be handled as automatically as possible.
  • Programmers should be able to display the splash screen anytime.
  • It should be possible to block until the window gets dismissed.

Figure 1 shows an example of JSplash in action.

Figure 1: JSplash Example Window.

Figure 1: JSplash Example Window.

If you look at the source code, you’ll see that the image handling is done through the ImageIcon class and that the JLabel component is used to frame the picture. We use a simple BevelBorder to make sure the image is visually in the foreground and adjust the JWindow bounds accordingly. The constructor first loads the image file, then calculates the position and size required to center the window with setBounds.

Once the image is loaded and the window is properly positioned, we register the JSplash class as a KeyListener and MouseListener, registering the parent as a MouseListener as well. We then create a JFC Timer which has the specified timeout in milliseconds. The Timer, which never repeats, will send an ActionEvent when the time elapses, so we register to receive this event as an ActionListener.

With the exception of the block method, the rest of the code merely implements all the listener interfaces we registered for. We capture each of the relevant events and dismiss the window by making it invisible before calling the dispose method. The block method allows you to wait until the splash screen is dismissed before continuing, to avoid sequence problems. The calling thread will block until the splash screen disappears.

Listing 2 shows the JSplashTest class, which demonstrates an important trick you’ll have to apply to properly capture keyboard events. We first create an arbitrary application frame and then the JSplash window with the frame reference, image file name and timeout period as constructor arguments. After calling the JFrame show method, we request the focus again for the JSplash object. If you don’t do this, pressing a key is may trigger something in the main application frame without dismissing the splash screen first, not typically what you really want.

The JTips Window

The JTip requirements are fairly simple. Show the user a single text tip and allow them to move forward, cycling each of the tips in turn, and starting at the first one when we reach the end. We also permit them to dismiss the window and to turn off the automatic tips at application startup. Figure 2 shows the visual result using an example tip file.

Figure 2: JTips Example Window.

Figure 2: JTips Example Window.

Listing 3 shows the JTips source code. The constructor first sets default dimensions and centers the window on the screen. We store the filename and properties arguments, allocate a vector to store the tips and then call readTipsFile to handle the file content. The rest of the constructor sets up the many panels that create the visual design. Figure 3 shows how these panels are nested to achieve the desired effect.

Figure 3: Nested Panels in JTips.

Figure 3: Nested Panels in JTips.

We use these many panels to control positioning and the effects of resizing. The shaded left icon panel, title and button panel always surround the tips panel and get repositioned accordingly if the window size changes. The navigation panel keeps the buttons grouped together to the bottom right of the window. The show check box is always at the bottom left.

Most of the remaining code in JTips is dedicated to handling the tip text. We use the tips Vector to store the sequence of tips read from the file and the Properties object to store the current position in the list. This design choice was made to make using configuration files as easy as possible. Normally, you would save the application-related properties in a file and load it when the program started. By permitting this value to be passed into JTips, it keeps the coupling to a minimum. JTips stores two properties called "tips.index" and "tips.show".

The readTipsFile method opens the tips file and reads each line before closing it. Each line is preprocessed by replaceParagraphMarkers before being put into the tips vector. The replacement code recognizes the "\p" and replaces it with two new line ("\n") characters. Since each tip has to be on a single line, this allows you to create multi-paragraph tips when you need to.

A number of utility methods are also implemented. The getNext and setNext methods are accessors for the index property. The getShow and setShow methods do the same for the show property. The increment method cycles to the next index position and saves the new value. The nextTip method actually takes the text from the given index position and sets it to be displayed, then calls increment to move the index position forward.

Finally, we catch button events with the actionPerformed method, which implements the ActionListener interface. The three buttons toggle the show state, go to the next tip or cancel (making the window invisible). You can redisplay the window by using the show or setVisible methods. The startup method does this automatically if the show property is on. It can be used in your main clause to make this virtually transparent.

Listing 4 shows the source code for the EdgeBorder class. The code is long but simple and implements the Border interface. We use it in JTips to draw the line at the bottom of the title panel. EdgeBorder draws an etched line to the north, south, east or west, with the etching either raised or lowered. Although we only need the lowered north variation, it doesn’t make sense to implement a border without a proper design.

The EdgeBorder class has to implement the getBorderInsets method as part of the Border interface. We return a two (2) pixel inset on the selected edge. We also implement isBorderOpaque to always return true and the paintBorder method to draw the actual border. The code uses case statements to decide which lines to draw on the selected side. Each of the two lines are shaded based on the background color of the component in which the border is used.

Listing 5 shows the code for TipTextArea which subclasses JTextArea in order to control certain key properties more effectively. We set the font, an empty border to control the margins, make the editable property false and set the word wrap on and word-based. We also return false for the isFocusTraversable method so that the focus is never set on this control.

The ApplicationTest Harness

Listing 6 shows code for the ApplicationTest class. Most of the work is done in the main method intentionally, to demonstrate how you would set things up in a typical application. The constructor simply sets up the BackgroundPanel from Listing 7, which merely draws alternating blue and dark blue stripes in a panel so that you can see some contrast when you run it.

The ApplicationTest main clause creates an instance of itself and of the JSplash widget and then centers the main frame on the screen before showing it. We call requestFocus on the splash window to make sure we can capture keystrokes. We then create a new instance of JTips and call the JSplash block method to wait until the splash screen disappears before showing the tips windows. That’s all there is to it.

In a real application, you’ll probably want to add a pair of "Help" menu items to call up JSplash under "About…" and to redisplay the JTips window under "Tip of the Day…". We use the ellipsis (…) convention to tell the user they’ll be seeing a window when they select those menu entries.

Summary

This installment of the Widget Factory provides a pair of useful components you can add to your growing widget collection. The splash screen adds an element of professionalism to any application, offers an opportunity for custom branding and facilitates a strength of identity for the product with virtually no programming effort.

The JTips control is especially important in applications that are not so obvious when the user sees the interface for the first time. If your interface is difficult to use, you should consider a redesign, but if the interface is easy to use but non-obvious at first, a simple Tip of the Day is precisely what you need to help the user overcome those small, early barriers as painlessly as possible.

Listing 1

public class JSplash extends JWindow
  implements KeyListener, MouseListener, ActionListener
{
  public JSplash(JFrame parent, String filename, int timeout)
  {
    super(parent);
    ImageIcon image = new ImageIcon(filename);
    int w = image.getIconWidth() + 5;
    int h = image.getIconHeight() + 5;
		
    Dimension screen = 
      Toolkit.getDefaultToolkit().getScreenSize();
    int x = (screen.width - w) / 2;
    int y = (screen.height - h) / 2;
    setBounds(x, y, w, h);

    getContentPane().setLayout(new BorderLayout());
    JLabel picture = new JLabel(image);
    getContentPane().add("Center", picture);
    picture.setBorder(new BevelBorder(BevelBorder.RAISED));
		
    // Listen for key strokes
    addKeyListener(this);

    // Listen for mouse events from here and parent
    addMouseListener(this);
    parent.addMouseListener(this);
		
    // Timeout after a while
    Timer timer = new Timer(0, this);
    timer.setRepeats(false);
    timer.setInitialDelay(timeout);
    timer.start();
  }
	
  public void block()
  {
    while(isVisible()) {}
  }

  // Dismiss the window on a key press
  public void keyTyped(KeyEvent event) {}
  public void keyReleased(KeyEvent event) {}
  public void keyPressed(KeyEvent event)
  {
    setVisible(false);		
    dispose();
  }
	
  // Dismiss the window on a mouse click
  public void mousePressed(MouseEvent event) {}
  public void mouseReleased(MouseEvent event) {}
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}
  public void mouseClicked(MouseEvent event)
  {
    setVisible(false);		
    dispose();
  }

  // Dismiss the window on a timeout
  public void actionPerformed(ActionEvent event)
  {
    setVisible(false);		
    dispose();
  }
}

Listing 2

public class JSplashTest
{
  public static void main(String[] args)
  {
    JFrame frame = new JFrame("Splash Screen Test");	
    JSplash splash = new JSplash(frame, "JSplash.gif", 5000);
    splash.show();
		
    frame.setBounds(100, 100, 400, 400);
    frame.show();
		
    // Critical - this focus request must be here after
    // showing the parent to get keystrokes properly.
    splash.requestFocus();
  }
}

Listing 3

public class JTips extends JFrame
  implements ActionListener
{
  protected String filename;
  protected Properties properties;
  protected Vector tips;

  protected JButton next, close;
  protected JCheckBox show;
  protected JTextArea text;

  public JTips(String filename, Properties properties)
  {
    super("Tip of the Day");
    int w = 425;
    int h = 280;
    Dimension screen = 
      Toolkit.getDefaultToolkit().getScreenSize();
    int x = (screen.width - w) / 2;
    int y = (screen.height - h) / 2;
    setBounds(x, y, w, h);
		
    this.filename = filename;
    this.properties = properties;

    getContentPane().setLayout(new BorderLayout());
    tips = new Vector();
    readTipFile();

    JPanel iconPanel = new JPanel();
    iconPanel.setLayout(new BorderLayout());
    iconPanel.setBackground(Color.gray);
    iconPanel.setPreferredSize(new Dimension(53, 53));
    JLabel icon = new JLabel(new ImageIcon("tipsIcon.gif"));
    icon.setVerticalAlignment(JLabel.CENTER);
    icon.setHorizontalAlignment(JLabel.CENTER);
    icon.setPreferredSize(new Dimension(53, 53));
    iconPanel.add("North", icon);

    JPanel titlePanel = new JPanel();
    JLabel title = new JLabel("Did you know...");
    title.setBorder(new EmptyBorder(10, 10, 0, 0));
    title.setFont(new Font("Helvetica", Font.PLAIN, 18));
    titlePanel.setLayout(new BorderLayout());
    titlePanel.setBorder(new EdgeBorder(EdgeBorder.SOUTH));
    titlePanel.setPreferredSize(new Dimension(46, 46));
    titlePanel.add("Center", title);
		
    text = new TipTextArea();
    text.setBackground(getBackground());

    JPanel centerPanel = new JPanel();
    centerPanel.setLayout(new BorderLayout());
    centerPanel.add("North", titlePanel);
    centerPanel.add("Center", text);
		
    JPanel tipsPanel = new JPanel();
    tipsPanel.setLayout(new BorderLayout());
    tipsPanel.setBorder(
      new CompoundBorder(
        new EmptyBorder(10, 10, 0, 10),
        new BevelBorder(BevelBorder.LOWERED)));
    tipsPanel.add("Center", centerPanel);
    tipsPanel.add("West", iconPanel);

    getContentPane().add("Center", tipsPanel);
		
    JPanel buttonPanel = new JPanel();
    buttonPanel.setLayout(
      new FlowLayout(FlowLayout.RIGHT, 10, 10));
    buttonPanel.add(next = new JButton("Next Tip"));
    buttonPanel.add(close = new JButton("Close"));

    JPanel showPanel = new JPanel();
    showPanel.setLayout(
      new FlowLayout(FlowLayout.LEFT, 10, 10));
      showPanel.add(show =
        new JCheckBox("Show tips at startup", getShow()));
		
    JPanel navPanel = new JPanel();
    navPanel.setLayout(new BorderLayout());
    navPanel.add("East", buttonPanel);
    navPanel.add("West", showPanel);

    getContentPane().add("South", navPanel);

    next.addActionListener(this);
    close.addActionListener(this);
    show.addActionListener(this);

    nextTip();
  }

  private void increment()
  {
    int current = getNext() + 1;
    if (current >= tips.size()) current = 0;
    setNext(current);
  }
	
  private void nextTip()
  {
    text.setText((String)tips.elementAt(getNext()));
    increment();
  }
	
  private int getNext()
  {
    String prop = properties.getProperty("tips.index");
    if (prop == null)
    {
      setNext(0);
      return 0;
    }
    int next = Integer.parseInt(prop);
    return next;
  }
	
  private void setNext(int next)
  {
    properties.put("tips.index", "" + next);
  }

  public boolean getShow()
  {
    String prop = properties.getProperty("tips.show");
    if (prop == null)
    {
      setShow(true);
      return true;
    }
    return prop.equalsIgnoreCase("true");
  }
	
  public void setShow(boolean show)
  {
    properties.put("tips.show", "" + show);
  }

  public void readTipFile()
  {
    tips.removeAllElements();
    try
    {
      FileReader file = new FileReader(filename);
      BufferedReader input = new BufferedReader(file);

      String line;
      while ((line = input.readLine()) != null)
      {
        line = replaceParagraphMarkers(line);
        tips.addElement(line);
      }
		
      input.close();
      file.close();
    }
    catch (FileNotFoundException e)
    {
      tips.addElement("Tip file '" + filename + "' not found!");
    }
    catch (IOException e)
    {
      tips.addElement("Error reading '" + filename + "'");
    }
  }

  public String replaceParagraphMarkers(String line)
  {
    StringBuffer buffer = new StringBuffer(line);
    int pos;
    while ((pos = line.indexOf("\\p")) > -1)
    {
      buffer.setCharAt(pos, '\n');
      buffer.setCharAt(pos + 1, '\n');
      line = buffer.toString();
    }
    return line;
  }
	
  public void actionPerformed(ActionEvent event)
  {
    Object source = event.getSource();
    if (source == close)
    {
      setVisible(false);
    }
    if (source == next)
    {
      nextTip();
    }
    if (source == show)
    {
      setShow(show.isSelected());
    }
  }
	
  public void startup()
  {
    if (getShow()) setVisible(true);
  }
}

Listing 4

public class EdgeBorder implements Border, SwingConstants
{
  public static final int RAISED = 1;
  public static final int LOWERED = 2;
	
  protected int edge = NORTH;
  protected int lift = LOWERED;
	
  public EdgeBorder()
  {
    this(NORTH);
  }

  public EdgeBorder(int edge)
  {
    this.edge = edge;
  }

  public Insets getBorderInsets(Component component)
  {
    switch (edge)
    {
      case SOUTH: return new Insets(0, 0, 2, 0);
      case EAST: return new Insets(0, 2, 0, 0);
      case WEST: return new Insets(0, 0, 0, 2);
      default: return new Insets(2, 0, 0, 0);
    }
  }
	
  public boolean isBorderOpaque()
  {
    return true;
  }

  public void paintBorder(Component component,
    Graphics g, int x, int y, int w, int h)
  {
    if (lift == RAISED)
      g.setColor(component.getBackground().brighter());
    else
      g.setColor(component.getBackground().darker());
    switch (edge)
    {
      case SOUTH:
        g.drawLine(x, y + h - 2, w, y + h - 2);
      break;
      
      case EAST:
        g.drawLine(x + w - 2, y, x + w - 2, y + h);
      break;

      case WEST:
        g.drawLine(x + 1, y, x + 1, y + h);
      break;
			
      default:
        g.drawLine(x, y, x + w, y);
    }
    if (lift == RAISED)
      g.setColor(component.getBackground().darker());
    else
      g.setColor(component.getBackground().brighter());
    switch (edge)
    {
      case SOUTH:
        g.drawLine(x, y + h - 1, w, y + h - 1);
      break;

      case EAST:
        g.drawLine(x + w - 1, y, x + w - 1, y + h);
      break;

      case WEST:
        g.drawLine(x + 1, y, x + 1, y + h);
      break;

      default:
        g.drawLine(x, y + 1, x + w, y + 1);
    }
  }
}

Listing 5

public class TipTextArea extends JTextArea
{
  public TipTextArea()
  {
    super();
    setFont(new Font("Helvetica", Font.PLAIN, 12));
    setBorder(new EmptyBorder(10, 10, 10, 10));
    setWrapStyleWord(true);
    setEditable(false);
    setLineWrap(true);
  }

  public boolean isFocusTraversable()
  {
    return false;
  }
}

Listing 6

public class ApplicationTest extends JFrame
{
  public ApplicationTest(String title)
  {
    super(title);
    getContentPane().setLayout(new BorderLayout());
    getContentPane().add("Center", new BackgroundPanel());
  }
	
  public static void main(String[] args)
  {
    JFrame frame = new ApplicationTest("Application Test");
		
    JSplash splash = new JSplash(frame, "JSplash.gif", 10000);
    splash.show();
		
    Dimension dim = 
      Toolkit.getDefaultToolkit().getScreenSize();
    frame.setBounds(100, 50,
      dim.width - 200, dim.height - 150);
    frame.show();
		
    // Critical - this focus request must be here after
    // showing the parent to get the focus properly.
    splash.requestFocus();

    JTips tips = new JTips("jtips.tip", new Properties());
    splash.block();
    tips.startup();
  }
}

Listing 7

public class BackgroundPanel extends JPanel
{
  public BackgroundPanel()
  {
    super();
    setOpaque(true);
  }
	
  public void paintComponent(Graphics g)
  {
    int w = getSize().width - 1;
    int h = getSize().height - 1;
    for (int y = 0; y < h; y += 10)
    {
      g.setColor(Color.blue);
      g.fillRect(0, y, w, y + 5);
      g.setColor(Color.blue.darker());
      g.fillRect(0, y + 5, w, y + 10);
    }
  }
}