Chapter 7

Creating Smarter Forms

by Mark Wutka


CONTENTS

Smarter Forms

In the beginning, Web pages were not very lively. You could read information, click certain words and pictures, and view other unlively pages. Then, the forms interface came along and added some degree of interaction with a page. You were able to enter data and then click a button and send your information to a server, which would analyze what you sent and return the results. Unfortunately, these forms were also lacking a certain "lively" quality. All the error checking was left up to the server, as was any other form of interaction such as context-sensitive help. Java enables you to spice up your old Web forms. You can perform error checking before you ever send data to the server, drastically improving response time to the user and cutting down on server usage. You can also add context-sensitive help. You can even create dynamic forms that change depending on the other information added.

Creating Forms with the AWT

Java's AWT toolkit contains a set of GUI components that are very useful for constructing forms suitable for a Web page. All the form components provided in HTML have equivalent AWT components, so you can simulate any existing HTML form.

Listing 7.1 shows just such a form, a query entry form for the Lycos Web searching engine (http://www.lycos.com). It enables you to enter the keywords to search for, and some other search parameters, and then it sends the query to the Lycos server just as if you had used, the Lycos page.

Note
This applet is meant for demonstration purposes only. While it will function with the real Lycos server, it does not display the advertisements from the normal Lycos search page. Although you may consider this a plus, it really isn't. Advertisements keep companies like Lycos in business and allow them to provide these wonderful services to you at no cost. Please do not use this applet or any other program to thwart a company's advertising displays. It hurts everyone in the long run.


Listing 7.1  Source Code for LycosForm.java
import java.awt.*;
import java.applet.*;
import java.net.*;
import java.util.*;

// This applet demonstrates the use of AWT components as an
// alternative to the HTML forms interface. It creates a query
// for the Lycos search engine and displays the results using
// the showDocument method.

public class LycosForm extends Applet
{
     protected TextField queryString; // the terms to search for

     protected Choice matchTerms; // how many terms to match
     String matchTermValues[] = { "and", "or", "2", "3", "4",
          "5", "6", "7" };

     protected Choice matchStrength; // how good a match
     String matchStrengthValues[] = { ".1", ".3", ".5", ".7", ".9" };

     protected Choice resultCount; // how many matches to show
     String resultCountValues[] = { "10", "20", "30", "40" };

     protected Choice resultType;  // how much information to display
     String resultTypeValues[] = { "terse", "standard", "verbose" };

     protected Button searchButton; // perform the query

     public void init()
     {
     // Arrange the query form as a 3 horizontal grid elements
          setLayout(new GridLayout(3, 0));

     // Create the element with the query string and submit button
          add(createQueryPanel());

     // Create the element containing search options
          add(createSearchOptionsPanel());

     // Create the element containing display options
          add(createDisplayOptionsPanel());

     }

The AWT layout managers provide a reasonable way to place components on the screen without putting them in fixed positions. This allows your applet to adapt to different screen sizes. Unfortunately, it is often difficult to arrange the components the way you want them. The GridBagLayout class provides the most flexible way to arrange components, but it is often rather cumbersome to use. As an alternative to the GridBagLayout class, or even in conjunction with it, you can use different panels to group your components, nesting some panels within others. The LycosQuery class uses this technique. It creates a main panel that uses a grid layout with three rows. The first row is another panel that uses a flow layout, while the last two rows use two-column grid layouts.

Tip
Grid layouts expand components to fill all available space. If you want to maximize a component's size, the grid layout is a good choice. Flow layouts, on the other hand, don't adjust the component size, so they tend to use the minimum required space. Grid bag layouts let you choose either of these options.


Listing 7.1  Source Code for LycosForm.java (continued)
// createQueryPanel creates a panel containing a text field
// for query terms and the button used to send the query to Lycos

     protected Panel createQueryPanel()
     {
          Panel panel = rEw Panel();
          panel.setLayout(new FlowLayout(FlowLayout.LEFT));

          panel.add(new Label("Query: "));

          queryString = new TextField(30);
          panel.add(queryString);

          searchButton = new Button("Search");
          panel.add(searchButton);

          return panel;
     }

// createSearchOptionsPanel creates a panel containing the
// choices for the number of terms to match and the strength
// of the matches.

     protected Panel createSearchOptionsPanel()
     {
          Panel panel = new Panel();
          panel.setLayout(new GridLayout(0, 3));
          panel.add(new Label("Search Options:"));

          matchTerms = new Choice();
          matchTerms.addItem("match all terms (AND)");
          matchTerms.addItem("match any term (OR)");
          matchTerms.addItem("match 2 terms");
          matchTerms.addItem("match 3 terms");
          matchTerms.addItem("match 4 terms");
          matchTerms.addItem("match 5 terms");
          matchTerms.addItem("match 6 terms");
          matchTerms.addItem("match 7 terms");

          matchTerms.select(1);     // default on the OR option
          panel.add(matchTerms);

          matchStrength = new Choice();
          matchStrength.addItem("loose match");
          matchStrength.addItem("fair match");
          matchStrength.addItem("good match");
          matchStrength.addItem("close match");
          matchStrength.addItem("strong match");

          matchStrength.select(0); // default on the loose match
          panel.add(matchStrength);

          return panel;
     }

// createDisplayOptionsPanel creates a panel containing the choices for
// the number of matches returned and the amount of detail to return.

     protected Panel createDisplayOptionsPanel()
     {
          Panel panel = new Panel();
          panel.setLayout(new GridLayout(0, 3));
          panel.add(new Label("Display Options:"));

          resultCount = new Choice();
          resultCount.addItem("10 results per page");
          resultCount.addItem("20 results per page");
          resultCount.addItem("30 results per page");
          resultCount.addItem("40 results per page");

          resultCount.select(0);     // Default to 10 results per page
          panel.add(resultCount);

          resultType = new Choice();
          resultType.addItem("Summary Results");
          resultType.addItem("Standard Results");
          resultType.addItem("Detailed Results");

          resultType.select(1);     // Default to Standard Results
          panel.add(resultType);

          return panel;
     }

The URLQuery class used in this next part of the LycosQuery class was introduced in the section, "Performing a Query with GET," in Chapter 6, "Communicating with a Web Server." It allows you to create an HTTP query from an URL and a properties table containing the query parameters. It would be nice if you could examine the data coming back from the query and still let the browser display the actual HTML codes returned, but on most browsers you can't. You can either examine the data coming back and display it yourself from the Java program, or use showDocument to display the data directly.


Listing 7.1  Source Code for LycosForm.java (continued)
// sendRequest uses the URLGet class to create a CGI Query to Lycos.

     protected void sendRequest()
     {
          Properties queryProps = new Properties();

          queryProps.put("query", queryString.getText());
          queryProps.put("matchmode", matchTermValues[
               matchTerms.getSelectedIndex()]);
          queryProps.put("minscore", matchStrengthValues[
               matchStrength.getSelectedIndex()]);
          queryProps.put("maxhits", resultCountValues[
               resultCount.getSelectedIndex()]);
          queryProps.put("terse", resultTypeValues[
               resultType.getSelectedIndex()]);
         
          try {
               URL lycosURL = new URL(    
                    "http://www.lycos.com/cgi-bin/pursuit");
              
               URL fullURL = URLQuery.createQuery(lycosURL,
                    queryProps);
              
               getAppletContext().showDocument(fullURL);
          } catch (Exception e) {
               e.printStackTrace();
          }
     }

     public boolean action(Event evt, Object whichAction)
     {

// If someone pressed the button, send the request

          if (evt.target == searchButton) {
               sendRequest();
               return true;
          }
          return false;
     }
}

Figure 7.1 shows the original version of the Lycos query form.

Figure 7.1 : The Lycos search engine is a popular Web search tool.

Figure 7.2 shows a mimic of an HTML form.

Figure 7.2 : You can mimic any HTML form in Java.

You may be wondering why you should go through the trouble of creating a Java applet that presents a form when it is easier to define one in HTML. If you are simply presenting a form, with no help facility and no error checking, go ahead and do it in HTML. The real advantage of Java comes when you need to do things beyond the basic form facilities in HTML.

Checking for Errors on the Client Side

You can increase the response time of your forms and lessen the load on your server if you put error checking into your Java forms. When you do forms in HTML, there is no way to check to make sure the data is correct before you send it off to the server. You put all the responsibility for error checking on the server's shoulders. This also means that the user has to wait for the request to go out to the server, and a response to come back, before he knows that there was something wrong.

If you use Java to do error checking, you will make the user happier because he will know instantly that he has entered incorrect data. You will also decrease the load on your server because it is no longer handling any incorrect data (hopefully). As your forms become more and more complex, the need for error checking grows dramatically.

In the example of the Lycos query form, the only place in which you can make an error entering data is in the query string itself. If you fail to enter any keywords, the search engine has nothing to look for. While Lycos normally just presents you with another form, this Java version of the query form pops up a dialog box reminding you that you need to enter keywords in the query entry area. The only place you need to change the old LycosForm class is in the action method. You make it call a checkRequest method instead of sendRequest. The checkRequest method verifies the form, and if everything is correct, it calls sendRequest. Here are the updated action and sendRequest methods:

        public boolean action(Event evt, Object whichAction)
        {
// If someone pressed the button, send the request

                if (evt.target == searchButton) {
                        checkRequest();
                        return true;
                }
                return false;
        }

        protected void checkRequest()
        {
                if (queryString.getText().length() == 0) {
                        OKDialog.createOKDialog(
                                "Please enter a list of terms to search for");
                        return;
                }

                sendRequest();
        }

This is actually a pretty minor form of error checking. On more advanced forms, you may need to check to see that information entered in one section is consistent with information entered in another area. For example, you might have a "sex" field on your form and a "maiden name" field somewhere else. If sex was "male," the maiden name doesn't apply. Your error checking routine would check to make sure that if you entered something under "maiden name," you had better be female. You can avoid some situations such as this one by creating dynamic forms, which are discussed later in this chapter.

Adding Context-Sensitive Help

Context-sensitive help is an incredibly useful feature, especially on the day when a software product comes with a 500-page manual. While you should strive towards making your program completely intuitive, requiring no special training or documentation, that isn't always possible. Context-sensitive help can lessen the need for other documentation and is much more timely and relevant than an online user's manual.

In case you are unclear about what "context-sensitive help" is, it is simply help on what you are currently doing. For example, if you are entering text in a field and you suddenly press the help key, you should expect to get help for the text field you are entering. Many software products require that you pop up the online manual and skip to the page discussing the field you want information on. If you know what the user is currently doing, you know what to tell them when they ask for help.

The way you present context-sensitive help is up to you. One very useful method, which is also quite passive, is to display a one-line message at the bottom of the screen giving a quick description of the area where the mouse is. For example, when you are looking at a Web page and pass the mouse over a link, your browser may display the destination URL for that link at the bottom of the page. This is a form of context-sensitive help. You can also define a particular key to be the "help" key. Tell the user to press that key any time he needs help. The F1 key in many software packages is the help key; you should seriously consider making it the help key in your applets, too. Remember that you want to give all your applets and applications a similar feel in the same way that most cars have a similar feel. It doesn't take long to figure out how to drive a car you've never seen before. It should be that way with software.

Listing 7.2 shows a HelpDialog class that is useful for popping up screens containing help text. The help dialog is a simple OK dialog box-it displays a text message and a button labeled "OK" which, when clicked, makes the dialog box disappear. One thing to keep in mind when you want to create dialog boxes is that you must have a parent frame for the dialog box. When you are running an applet, you can't normally access the applet's parent frame. The HelpDialog class addresses this problem by creating its own frame. It saves the frame in a static variable so it doesn't have to create a new frame the next time it needs to create a dialog window.

You can actually access the parent frame for an applet. Sometimes it will work exactly like you want. It usually works for dialogs, but it fails miserably on some platforms when you try to create a menu for the parent frame. You can use the getParent method from the component class to trace back up through the component hierarchy to find the applet's parent frame. The following code fragment finds an applet's parent frame:

Component parentFrame = getParent();
while ((parentFrame != null) &&
	!(parentFrame instanceof Frame)) {
	parentFrame = parentFrame.getParent();
}
Frame myFrame = (Frame) parentFrame;

At this point, myFrame would either contain the parent frame of the applet, or null if it couldn't find the parent frame.


Listing 7.2  Source Code for HelpDialog.java
import java.awt.*;

// The HelpDialog class is a variation on the OKDialog class.
// It allows you to create an OK dialog with a textarea instead
// of a label. You can use this to display help text.

public class HelpDialog extends Dialog
{
     protected Button okButton;
     protected static Frame createdFrame;

     public HelpDialog(Frame parent, String message)
     {
          super(parent, false);     // Must call the parent's constructor
         
// Create the OK button and the message to display
          okButton = new Button("OK");
          TextArea helpInfo = new TextArea(message, 10, 40);
          helpInfo.setEditable(false);

          setLayout(new BorderLayout());

          add("Center", helpInfo);

          add("South", okButton);

          resize(500, 300);
     }

// The action method just waits for the OK button to be clicked; 
// when it is, it hides the dialog, causing the show() method to return
// back to whoever activated this dialog.

     public boolean action(Event evt, Object whichAction)
     {
          if (evt.target == okButton)
          {
               hide();
               if (createdFrame != null)
               {
                    createdFrame.remove(this);
                    createdFrame.hide();
                    dispose();
                    return true;
               }
          }
          return true;
     }

// Shortcut to create a frame automatically, the frame is a static variable
// so all dialogs in an applet or application can use the same frame.

     public static void createHelpDialog(String helpText)
     {
// If the frame hasn't been created yet, create it
          if (createdFrame == null)
          {
               createdFrame = new Frame("Help");
          }
// Create the dialog now
          HelpDialog helpDialog = new HelpDialog(createdFrame, helpText);

// Shrink the frame to nothing
          createdFrame.resize(0, 0);

// Show the dialog
          createdFrame.show();
          helpDialog.show();
     }
}

In addition to the HelpDialog class, you need a way to assign help information directly to your AWT components. It would have been nice if Sun had built that right into the AWT, and maybe they will in the future, but for now you have to do it yourself. You could subclass all the AWT components to support help if you really had nothing better to do for a month or two, but there are easier ways. One simple way is just to store the components and their corresponding help text in a hash table. Whenever someone requests help from within an AWT component, look in the table and see if you have defined any help for that component. Listing 7.3 shows the HelpSystem class that enables you to assign help text to AWT components. It also contains a method to display the help for a component, but it makes no assumptions on how you actually request the help in the first place.


Listing 7.3  Source Code for HelpSystem.java
import java.awt.*;
import java.util.*;

// Help system is a container for help strings. You can add
// and remove help strings for components. It also provides
// a doHelp method that actually pops up the help dialog.

public class HelpSystem extends Object
{
     Hashtable helpTable;

     public HelpSystem()
     {
          helpTable = new Hashtable();
     }

     public void addHelp(Component comp, String text)
     {
          helpTable.put(comp, text);
     }

     public void removeHelp(Component comp)
     {
          helpTable.remove(comp);
     }

     public boolean doHelp(Component comp)
     {
          if (comp == null) return false;

          String helpString = (String) helpTable.get(comp);

          if (helpString == null) {
               return false;
          }

          HelpDialog.createHelpDialog(helpString);

          return true;
     }
}

Now that you have a way to display help and a way to map help strings to components, you need to add some sort of help key to your applet. Going back to the Lycos search form applet, you can modify it to use F1 as the help key. The AWT components are polite enough to ignore keyDown events for keys they do not recognize, and they all leave the F1 key alone. You can trap the F1 key in your applet and display the appropriate help text. To add context-sensitive help to the LycosForm class, you need to create an instance of the help system. Since there are several methods that actually use the help system, you declare it as an instance variable:

protected HelpSystem helpSystem = new HelpSystem();

Next, for each component that will have a help screen, you add the component to the help system. For example, once you create the queryString text field, you can add a help string for it with the following code fragment:

helpSystem.addHelp(queryString,
"QUERY HELP\nEnter the words you want to search\n"+
"for separated by spaces. Avoid common words like\n "+
"\"the\" or \"and\".");

The trickiest part of implementing the help system is grabbing the F1 key and figuring out which component the user wants help on. When you receive keyboard events, you are given an x and y coordinate where the keystroke occurred. Unfortunately, this does not really indicate where the mouse was when you pressed the key. The x and y coordinates are bounded by the component that currently has the keyboard focus. For context-sensitive help, you don't want the user to have to move the keyboard focus to another component before requesting help. If this were the case, they would have to click a button before they could get help for that button. What you must do, instead, is track the movement of the mouse all the time. You can do this very simply by creating two instance variables in your class, mouseX and mouseY:

protected int mouseX;	// the current X coord of the mouse
protected int mouseY;	// the current Y coord of the mouse

Next, you override the mouseMove method. This method is called whenever the mouse moves. You simply copy the x and y coordinates of the mouse and return:

public boolean mouseMove(Event evt, int x, int y)
{
mouseX = x;
mouseY = y;
return false;
}

Notice that you return false from the mouseMove method. This indicates that you haven't actually handled the mouse movement event, allowing the event to be passed to another component. If you do not want another component to see the mouse movement event, you should return true instead.

The hardest part of implementing this context-sensitive help system is determining which component the user wants help on. The problem here is that you have to take the x and y coordinates of the mouse and locate the component at those coordinates.

The locate method does this, sort of. The locate method takes an x and y coordinate and returns the component at those coordinates. It only looks one level deep in the component hierarchy, however. If you are using nested panels, as the LycosForm applet does, the locate method will only return the panel enclosing the component you really want.

The solution for this problem is simple. If the locate method returns a container, you use the locate method in that container. You keep repeating the process until locate returns a component that is not a container.

There is one additional little sticking point here. The locate method expects the x and y coordinates to be relative to the container you are searching. The first time you call locate, everything is fine, since the mouse x and y coordinates are relative to your applet. After that, you have to adjust them to be relative to the container returned by locate. For example, suppose you had mouse coordinates of 100, 50 and the locate method returned an instance of the Panel class for those coordinates. Suppose that the panel's upper-left corner was at 65, 45. You would subtract the panel's coordinates from the original mouse coordinates, giving a new location of 35,5. Now you call the locate method in the panel with the new coordinates. You can use the location method to get the coordinates of the upper-left corner of any component. Listing 7.4 shows a keyDown method for the LycosForm applet that uses this technique to identify the component where the F1 key was pressed.


Listing 7.4  Source Code for the keyDown Method in LycosForm3.java
public boolean keyDown(Event evt, int ch)
{
	if (ch == Event.F1) {
		int x = mouseX;
		int y = mouseY;

// Find out which component this x,y is inside
		Component whichComp = locate(x, y);

// If the component is a container, descend into the container and
// find out which of its components contains this x,y

		while (whichComp instanceof Container) {

// If you have to search within a container, adjust the x,y to be relative
// to the container.
			x -= whichComp.location().x;
			y -= whichComp.location().y;
			Component nextComp = whichComp.locate(x, y);

// if locate returns the component itself, you're done
			if (nextComp == whichComp) break;
			whichComp = nextComp;
		}

// Display any available help on the component
		helpSystem.doHelp(whichComp);
	}
	return false;
}

Figure 7.3 shows the LycosForm3 applet in action with a Help dialog box displayed.

Figure 7.3 : Context-sensitive help screens make your applets easier to use.

Creating Dynamic Forms

It's funny that with all the advanced technology running on the desktop today, the methods of recording information haven't really changed. Most online forms are just electronic versions of printed forms. This is really a shame because we now have the ability to create forms that adapt to the information you are entering.

For example, suppose you are creating a personal information form containing all the typical pieces of information associated with a person. If you look at a typical form of this type, you'll see many sections with instructions such as "Fill in this section only if married." No computerized form should have instructions such as that-not when there are so many ways to avoid it.

If you really must have your form look exactly the same, no matter what information is being entered, consider selectively enabling and disabling components if they apply. Listing 7.5 shows a very brief example of this technique.


Listing 7.5  Source Code for DynamicDisable.java
import java.awt.*;
import java.applet.*;

// This applet demonstrates the technique of enabling and
// disabling components based on the values of other components.
// Specifically, it has a choice for sex of "Male" or "Female".
// It also has a maiden name field that is enabled only if sex is
// "Female".

public class DynamicDisable extends Applet
{
     TextField maidenName;
     Choice sex;

     public void init()
     {

// Create the sex choice
          sex = new Choice();
          sex.addItem("Male");
          sex.addItem("Female");

// Default to male
          sex.select(0);

          add(sex);

// Create maiden name and disable it because sex defaults to male

          maidenName = new TextField(20);
          maidenName.disable();

          add(maidenName);
     }

     public boolean action(Event evt, Object whichAction)
     {

// If you get an action event on sex, look at the current
// value and enable or disable maiden name accordingly

          if (evt.target == sex)
          {
// If the index is 0, "male" has been selected, so disable maiden name
               if (sex.getSelectedIndex() == 0) {
                    maidenName.disable();

// otherwise, enable maiden name
               } else {
                    maidenName.enable();
               }
               return true;
          }
          return false;
     }
}

This technique doesn't provide much of an improvement over paper forms, however. You could still be looking at a huge document full of components, some of which are enabled and some which are disabled. It would be a lot kinder to the user to show him only the items he actually needs to fill in. In other words, rather than just disabling a component, hide it-make it invisible. Hiding has its drawbacks, however. When you hide a component, the layout manager will change the layout of the components. If you aren't using a layout manager, this won't be a problem. If you are using a layout manager, pay special attention to how the form changes when you show and hide various components. You may want to perform a mixture of disabling and hiding. Listing 7.6 shows a very brief example of how to hide and show components dynamically, using the same components as the example in Listing 7.5. Notice that you must call the validate method after hiding or showing a component. This causes the layout manager to recompute the component positions.


Listing 7.6  Source Code for DynamicHide.java
import java.awt.*;
import java.applet.*;

// This applet demonstrates the technique of hiding and
// showing components based on the values of other components.
// Specifically, it has a choice for sex of "Male" or "Female".
// It also has a maiden name field that is visible only if sex is
// "Female".

public class DynamicHide extends Applet
{
     TextField maidenName;
     Choice sex;

     public void init()
     {

// Create the sex choice
          sex = new Choice();
          sex.addItem("Male");
          sex.addItem("Female");

// Default to male
          sex.select(0);

          add(sex);

// Create maiden name and hide it because sex defaults to male

          maidenName = new TextField(20);
          maidenName.hide();

          add(maidenName);
     }

     public boolean action(Event evt, Object whichAction)
     {

// If you get an action event on sex, look at the current
// value and show or hide maiden name accordingly

          if (evt.target == sex)
          {
// If the index is 0, "male" has been selected, so hide maiden name
               if (sex.getSelectedIndex() == 0) {
                    maidenName.hide();
                    validate();

// otherwise, show maiden name
               } else {
                    maidenName.show();
                    validate();
               }
               return true;
          }
          return false;
     }
}

The CardLayout layout manager is another good tool for dynamic form construction. It lets you create a stack of different containers (usually panels), only one of which is displayed at any time. By using a card layout, you can create all your panels ahead of time and add them to the card layout. Then, whenever you want to display a specific panel, you tell the card which panel to display. For example, you may have panels that display information on Moe, Larry, and Curly. Listing 7.7 shows a simple example program that uses a card layout and some buttons to select the specific card.


Listing 7.7  Source Code for CardExample.java
import java.applet.*;
import java.awt.*;

// This applet demonstrates how a card layout can be used to
// display different panels. The panels are given names when
// added to the card layout. There are buttons at the bottom of
// the screen with names corresponding to the panel names. When
// you press a button, it tells the card layout to display the
// card with the same name as the button.

public class CardExample extends Applet
{
	CardLayout cards;
	Panel stoogePanel;

	public void init()
	{

// Need a border layout to have the stooge panel in the center and
// the buttons at the bottom.

		setLayout(new BorderLayout());

// Create the main display panel
		stoogePanel = new Panel();

// Give the main display panel a card layout
		cards = new CardLayout();
		stoogePanel.setLayout(cards);

// Create the panels for the different cards. For demo purposes, each
// panel just has a label on it.

		Panel moePanel = new Panel();
		moePanel.add(new Label("Moe"));

		Panel larryPanel = new Panel();
		larryPanel.add(new Label("Larry"));

		Panel curlyPanel = new Panel();
		curlyPanel.add(new Label("Curly"));

// Add the separate panels to the stoogePanel giving them their
// own card names.

		stoogePanel.add("Moe", moePanel);
		stoogePanel.add("Larry", larryPanel);
		stoogePanel.add("Curly", curlyPanel);

// Put the stoogePanel in the middle of the applet's border layout

		add("Center", stoogePanel);

// Now create a row of buttons for selecting the different cards. The
// button names must match the names used above.

		Panel selectorPanel = new Panel();
		selectorPanel.add(new Button("Moe"));
		selectorPanel.add(new Button("Larry"));
		selectorPanel.add(new Button("Curly"));

// Put the row of buttons at the bottom part of the border layout
		add("South", selectorPanel);
	}

	public boolean action(Event evt, Object whichAction)
	{

// If the action event is for a button, whichAction is the button's
// label, which is also the name of a card in this program. We just
// tell the card layout to show the appropriate card.

		if (evt.target instanceof Button) {
			cards.show(stoogePanel, (String) whichAction);
			return true;
		}
		return false;
	}
}

Figure 7.4 shows the CardExample applet in action. The buttons along the bottom select the different card, which simply contain a single label.

Figure 7.4 : A card layout enables you to display one of several panels.

When you are creating dynamic forms, you can group sections of your forms onto different cards. You can create different methods for going from one card to the next, like having a master index of the different cards, or putting Next and Prev buttons on each card. If you want to disable a section of the form, don't make that section's card available.

For example, suppose you have a part of the form for entering marriage information-date, place, witnesses, and so on. If a person is single, you don't want to present that part. You can remove it from the set of cards in your card layout.

Loading Another URL from an Applet

You may have noticed that several applets in the last two chapters actually open up URLs on the Web and display their contents in the browser. They all use the showDocument method in the AppletContext class. You can use the showDocument class to give your Java applets the same connectivity to the rest of the Web that any Web page has. This also enables you to create new and unique ways to access Web pages.

The showDocument method can take a second parameter, which is the target frame for the URL. Your Java applet can open up an URL in its own frame, its parent frame, the top-most frame, a brand new frame, or a particular named frame. You can use this ability to create interesting Web page layouts. For instance, you can create an index applet in Java that provides some neat new way of listing URLs. You could run this applet in a narrow frame on the left side of the page, leaving the rest of the page for the frame where the selected URL will be loaded.

Note
One of the major features lacking in Java is the ability for a Java program to gen-erate HTML data that is displayed by the browser. The showDocument method in Netscape bypasses the URLConnection class and goes straight to a native method to load an URL. You should be able to generate your own HTML data in HotJava if you create a special URLConnection class and define a protocol type for it. We can only hope there will soon be a way to do this in all browsers.

Creating Image Maps with Hot Spots

Image maps were a neat addition to the Web a few years ago. Rather than a list of textual links, you could open up an URL by clicking a particular part of an image. These image maps still have limitations, however, because they are not very interactive. You can create a Java image map, however, that contains hotspots-areas that light up when the mouse passes over them. You can also add context-sensitive help, which is always a nice thing to have.

To create an image map with hot spot, you need a way to define what a "hot spot" is. You could simply create a class that represented an area of the image. This class would also be responsible for displaying whatever should appear when the mouse passes over the hot spot. Listing 7.8 shows an abstract class that defines the methods necessary to implement such a class.


Listing 7.8  Source Code for ImageRegion.java
import java.awt.*;

// ImageRegion is an abstract definition of the region
// area supported by the ImageMap class.
public abstract class ImageRegion extends Object
{
     public ImageRegion()
     {
     }

// select is called when you click the mouse within a region
     public void select()
     {
     }

// mouseEnter is called when the mouse enters a region
     public void mouseEnter()
     {
     }

// mouseLeave is called when the mouse leaves a region
     public void mouseLeave()
     {
     }

// getBoundingBox should return the smallest rectangle that
// completely encloses this region.

     public abstract Rectangle getBoundingBox();

// inside returns true if x,y is within this region

     public abstract boolean inside(int x, int y);

// paint is used to draw any hotspot popup information
     public void paint(Graphics g)
     {
     }
}

Because the ImageRegion class is an abstract class, you need something concrete to actually implement a region. You will almost certainly need to define a rectangular region at some point. Actually, it is trivial to extend a rectangular region to be a polygon region. Listing 7.9 shows an implementation of ImageRegion that supports polygon regions.


Listing 7.9  Source Code for ImageRegionPoly.java
import java.awt.*;

// ImageRegionPoly implements a rectangular region for
// use with the ImageMap class.

public class ImageRegionPoly extends ImageRegion
{
     Polygon boundary;

     public ImageRegionPoly()
     {
          boundary = new Polygon();
     }

     public ImageRegionPoly(Polygon p)
     {
          boundary = p;
     }

     public Rectangle getBoundingBox()
     {
          return boundary.getBoundingBox();
     }

     public boolean inside(int x, int y)
     {
          return boundary.inside(x, y);
     }
}

You may also have a need for a circular region. The ImageRegionCircle class in Listing 7.10 implements a circular region.


Listing 7.10  Source Code for ImageRegionCircle.java
import java.awt.*;

// ImageRegionCircle defines a circular region for use
// with the ImageMap class.

public class ImageRegionCircle extends ImageRegion
{
     Point center;
     int radius;

     public ImageRegionCircle()
     {
          center = new Point(0, 0);
          radius = 0;
     }

     public ImageRegionCircle(Point center, int radius)
     {
          this.center = center;
          this.radius = radius;
     }

     public Rectangle getBoundingBox()
     {
          return new Rectangle(center.x - radius, center.y - radius,
               2*radius, 2*radius);
     }

// Use the distance formula to determine if a point is inside or not.
// If the distance between x,y and the center of the region is <= the
// radius of the circle, the point is within the region.

     public boolean inside(int x, int y)
     {
          int xd = center.x - x;
          int yd = center.y - y;
         
          int dist = (int) Math.sqrt(xd*xd+yd*yd);

          return dist <= radius;
     }
}

Now that you have a method for defining a region in an image, you need a way to display an image, add these regions to it, and track the mouse to see when it hits a region. The ImageMap class in Listing 7.11 does just that. It also shows you how to define a canvas that displays an image.


Listing 7.11  Source Code for ImageMap.java
import java.awt.*;
import java.util.*;

// The image map is a canvas that displays an image and supports
// hotspots. The hotspots are defined by subclasses of ImageRegion.
// There can only be one hotspot active at a time. Whenever a hotspot
// is active, its paint method is called so it can paint any popup
// information. You could display a little box of text saying what
// the hotspot does, for instance. The default paint method for a
// hotspot does nothing.

public class ImageMap extends Canvas
{
     Image image;
     Vector regions;
     ImageRegion selectedRegion;
     boolean moved;
    
     public ImageMap(Image image)
     {
          this.image = image;
          regions = new Vector();
          moved = true;
     }

// The size of the Canvas is defined by the size of the image.

     public Dimension minimumSize()
     {
          return new Dimension(image.getWidth(this),
               image.getHeight(this));
     }

     public Dimension preferredSize()
     {
          return minimumSize();
     }

     public Dimension size()
     {
          return minimumSize();
     }

     public void addRegion(ImageRegion region)
     {
          regions.addElement(region);
     }

     public void removeRegion(ImageRegion region)
     {
          regions.removeElement(region);
          if (region == selectedRegion) {
               selectedRegion = null;
          }
     }

// To repaint this canvas, redraw the image. Then, if there is a hotspot
// active, call that hotspot's paint method.

     public void paint(Graphics g)
     {
// Draw the image
          g.drawImage(image, 0, 0, this);

          if (selectedRegion != null) {

// Find the bounding box for the current region (hotspot)
               Rectangle r = selectedRegion.getBoundingBox();

// Create a graphics context for the bounding box
               Graphics regionGraphics = g.create(r.x, r.y,
                    r.width, r.height);

// Let the region paint its little area
               selectedRegion.paint(regionGraphics);
          }
     }

// Flicker-free update

     public void update(Graphics g)
     {
          paint(g);
     }

The next section of the ImageMap class demonstrates a very important concept in object-oriented design. The ImageMap class implements a framework that allows you to plug in different ImageRegion objects. You can add many new types of ImageRegion objects without changing the ImageMap class itself. It is very important to correctly assign class responsibilities in your design. In this case, the ImageMap class is responsible for displaying the master image, or background image. It is also responsible for tracking mouse movements and passing them on to affected regions. The ImageRegion class is responsible for displaying itself on the map if necessary, and for responding to a mouse click.

Tip
When designing classes for an application, you want to be able to add functionality by adding new classes, and not by changing existing classes. Try to identify things that may change and let those things be implemented by a separate class.


Listing 7.11  Source Code for ImageMap.java (continued)
// Need to watch the mouse movement to see if the mouse hits
// a hotspot or not.

     public boolean mouseMove(Event evt, int x, int y)
     {
          moved = true;     // kludge to handle mouse-click problem

// Quick shortcut here, see if you're still in the current region
          if ((selectedRegion != null) &&
               selectedRegion.inside(x, y)) {
               return true;
          }
    
// If there's a current region and you're not in it, tell the old
// region that the mouse left it.

          if (selectedRegion != null) {
               selectedRegion.mouseLeave();
               selectedRegion = null;
          }
              
// Check all the regions to see if the mouse is within any of them.
// If two overlap, it's on a first come, first served basis - that is,
// the first region that was added has priority.

          Enumeration e = regions.elements();
          while (e.hasMoreElements()) {
               ImageRegion r = (ImageRegion) e.nextElement();

// See if the mouse's x,y is within the region's area
               if (r.inside(x, y)) {
                    selectedRegion = r;
                    r.mouseEnter();
                    break;
               }
          }

          repaint();
          return true;
     }

// Mouse down handles mouse clicks, and also will keep track
// of mouse movement

     public boolean mouseDown(Event evt, int x, int y)
     {
// The moved flag is a kludge. Sometimes you'll get more than
// one mouse click. Assume that if the mouse doesn't move
// between clicks, the user doesn't want more than one click.

          if (!moved) return true;
          moved = false;

// Quick shortcut here
          if ((selectedRegion != null) &&
               selectedRegion.inside(x, y)) {
               selectedRegion.select();
               return true;
          }
    
          if (selectedRegion != null) {
               selectedRegion.mouseLeave();
               selectedRegion = null;
          }
              
          Enumeration e = regions.elements();
          while (e.hasMoreElements()) {
               ImageRegion r = (ImageRegion) e.nextElement();
               if (r.inside(x, y)) {
                    selectedRegion = r;
                    r.mouseEnter();
                    r.select();
                    break;
               }
          }
          repaint();
          return true;
     }
}

You may have noticed that the implementations of the image regions were incredibly small and didn't really seem to do anything. You are correct on both counts. To get any benefit out of the regions, you have to create subclasses that actually do something.

Suppose you want to create a map that has a set of circular hotspots that light up with the name of the city in that section of the map. You need to keep track of the name of the city and also implement a paint method that displays the city name. Because an image map isn't very useful if you can't select items, your city hotspot should also do something when you click it. Listing 7.12 shows a circular region that represents a city. When you click the region, it pops up an OK dialog box telling you which city you clicked.


Listing 7.12  Source Code for CityRegion.java
import java.awt.*;

// This class implements a special version of the
// ImageRegionCircle class to represent cities on a map.
// When the mouse gets within range of a city, the city name
// is displayed. When you click the city, it pops up a dialog
// box telling you what city you clicked.

public class CityRegion extends ImageRegionCircle
{
     String name;

     public CityRegion()
     {
     }

// You can specify either x,y to create a CityRegion or a Point

     public CityRegion(String name, int x, int y)
     {

// Set up the region as a circle with a radius of 30 pixels

          super(new Point(x, y), 30);     // radius of 30
          this.name = name;
     }

     public CityRegion(String name, Point p)
     {

// Set up the region as a circle with a radius of 30 pixels
          super(p, 30);     // radius of 30
          this.name = name;
     }

// Paint is called when the mouse is within this city's bounding
// area - for this class, defined as a circle of radius 30
// We just draw the city's name in blue. Note that the graphics
// area is bounded by the bounding box for the region (actually, the
// smallest rectangle that will enclose the area because the regions
// can be non-rectangular).

     public void paint(Graphics g)
     {
          g.setColor(Color.blue);
          g.drawString(name, 0, 35);
     }

// If you click a city, you'll get a dialog box
     public void select()
     {
          OKDialog.createOKDialog("You selected the city of "+name);
     }
}

Now that all the pieces of the puzzle are in, you can create an the image map for displaying these cities. Listing 7.13 shows the CityApplet class.


Listing 7.13  Source Code for CityApplet.java
import java.awt.*;
import java.applet.*;

// This applet demonstrates the use of the ImageMap class
// It loads a map of the U.S.A. and creates a set of regions
// for the map. The regions are implemented in the CityRegion class.
// The numbers for the city coordinates are approximate, and were
// determined through ocular analysis (I eyeballed the map).

public class CityApplet extends Applet
{
     public void init()
     {

// Load the map image
          Image usaImage = getImage(getDocumentBase(), "usa.gif");

// Be naughty and use the MediaTracker to make sure the map is loaded
          MediaTracker mt = new MediaTracker(this);
          mt.addImage(usaImage, 0);
         
          try {
               mt.waitForAll();
          } catch (Exception ignore) {
          }

// Create an image map object for the image
          ImageMap imageMap = new ImageMap(usaImage);

// Add city regions to the image map
          imageMap.addRegion(new CityRegion("Atlanta", 323, 202));
          imageMap.addRegion(new CityRegion("New York", 377, 118));
          imageMap.addRegion(new CityRegion("L.A.", 45, 196));
          imageMap.addRegion(new CityRegion("San Fran", 34, 164));
          imageMap.addRegion(new CityRegion("Seattle", 52, 74));
          imageMap.addRegion(new CityRegion("Dallas", 218, 236));
          imageMap.addRegion(new CityRegion("Chicago", 277, 123));
          imageMap.addRegion(new CityRegion("Miami", 367, 270));
          imageMap.addRegion(new CityRegion("Denver", 102, 143));

// Add the image map to the applet
          add(imageMap);
     }
}

Figure 7.5 shows the output from this applet.

Figure 7.5 : Image maps in Java can implement hot spots.