Chapter 20

Designing and Implementing Advanced Applets


CONTENTS


Java is much more than a tool for animating objects. Whether you are on a corporate intranet or the Internet, you can use Java to solve your real-world business problems today. This chapter takes you step by step through the development of an advanced applet called SpreadSheet.

SpreadSheet is an amazing applet that implements a full-featured spreadsheet in less than 900 lines of code, using many of the advanced features of the Java programming language. Whereas previous chapters focus primarily on programming concepts, this chapter's emphasis is on design concepts. The design of your applet is the key to its power and reusability. Examining the SpreadSheet applet should give you a better understanding of using text and graphics, manipulating text strings, using mathematical formulas, and processing user input.

Project Development and Planning

Creating quick solutions for problems is what most programmers and Web administrators do every day. However, quick solutions usually are not the best ones. If problems occur or you must modify the hack later, far too often you spend more time trying to rework your quick solution than it would have taken to develop the program the right way in the first place. As you begin to develop more advanced applets, you should take the time to develop and plan them carefully.

Planning involves determining the steps necessary to complete the project. After you plot out the steps that will take you through project completion, you can group the steps into phases. The duration of each phase should be relevant to the size and complexity of the applet you are developing. Although you may be able to develop and implement an intermediate applet in a single day, more advanced projects are developed and implemented over a period of days, weeks, or months. Most projects include six phases: requirements, specification/analysis, planning, design, implementation, and testing.

Note
Because some phases are dependent on others, usually you should perform each phase in order. However, you can sometimes combine phases to suit the needs of the project you are developing. For the SpreadSheet applet, I combined the requirements, specification, and planning phases into a general phase called development and planning.

The Requirements Phase

The first step in the requirements phase is to develop a list of project needs by examining the purpose, scope, and audience of the project. Then you identify your reasonable expectations for the project. Finally, you translate these needs, goals, and purposes into project requirements.

The purpose statement should describe what type of applet you are creating and why you are creating it. The scope of the project relates to its size and functionality. The target user is the person or group for whom you are developing the applet. Often you will have primary and secondary target users.

Tip
If you have not created a project folder, you should create one now. The project folder, generally kept in both paper and electronic form, will hold everything related to the project. The paper form of the folder can be a three-ring binder or notepad, and the electronic version can reside on your hard drive as a directory.

The purpose, scope, and target user for the SpreadSheet applet are defined as follows:

Now make a list of project constraints. Key constraints for most projects include duration, budget, and size. The project's duration is usually constrained by deadlines or milestones you must meet; its budget is usually constrained by the amount of time and money you have to invest in the project; and the size is usually constrained by user requirements, performance issues, and the budget as well. The initial constraints for the SpreadSheet applet are

Now that you know the issues that are driving the development of the project, you can develop a list of needs for the project. Project needs include personnel, computer hardware and software, financial resources, and supplies. For the SpreadSheet applet, the list of needs is small:

The Specification/Analysis Phase

During the specification/analysis phase, you determine the inner workings of the applet. You can do this in a traditional manner through specification diagrams such as data-flow diagrams or state-transition diagrams, or you can simply define the necessary objects and the flow of data among them. The methodology you use during this phase will depend largely on the type of applet you are developing, the modeling tools you are using, and the development philosophy at your organization.

For the SpreadSheet applet, I borrowed some of the concepts used in structured analysis. Preliminary modeling in structured analysis is done in the environmental model, which helps you define the interfaces between your applet and the user. The environmental model includes three key components:

The great thing about the environmental model is that its components can be modified to meet the needs of just about any type of project. The first step in using this model is to define a purpose statement for the project. Because the statement of purpose is intended for management, it usually is no more than one paragraph. The brief statement of purpose defined in the "The Requirements Phase" section is expanded on for management use as follows:

The second step in using the environmental model is to create a context diagram that defines the external entities with which the applet interacts. As with most applets, a Web browser or external viewer acts as an intermediary between the user and the applet. The level of detail you use in the context diagram is up to you. For complex applets, you might want to depict each class object that the main object interacts with on this diagram as well.

Looking Ahead
For the SpreadSheet applet, I chose not to develop a context diagram. For an example of a context diagram, see the section titled "Defining the Necessary Objects" in Chapter 21, "Designing and Implementing Advanced Applications."

Instead of developing a context diagram for the SpreadSheet applet, let's take a preliminary look at the objects needed on the applet's primary frame. Break down the applet frame into regions, such as a title bar, menu bar, display areas, and input areas. You can then break these objects down into their main components.

For the SpreadSheet applet, the main regions on the frame are a title bar, an input area, and a display area made up of the rows and columns of the spreadsheet. These rows and columns are, in turn, made up of individual cells.

To aid the design process, you can create a diagram that depicts the location of these objects on the applet's primary frame. Figure 20.1 shows the preliminary design of the SpreadSheet applet's primary frame.

Figure 20.1 : Sketching the objects for the SpreadSheet applet's primary frame.

The final step in using the environmental model is to create an event list, a list of events the applet should support. You will use this list to help define objects for the applet. In general, most events that occur in applets are user driven. However, the type of applet you are developing ultimately determines the type of events in your list.

Applets by their nature as Web-published programs have a common set of events related to the core methods of the Applet class. These events are handled explicitly by the applet when defined in the applet or implicitly by the Applet class when not defined in the applet. Common events for applets include the following:

Most events for a spreadsheet are initiated when a user enters data, such as a label for a row or column, a mathematical formula, or a value for a cell. Because applets are published on the Web, events can also be triggered by parameters set in the HTML document used to display the applet. Events for the SpreadSheet applet include the following:

  1. Initializing the applet
  2. Accepting and processing parameter values
  3. Starting the applet with default values
  4. Running applet threads
  5. Performing calculations
  6. Creating the spreadsheet
  7. Painting the spreadsheet
  8. Accepting and process user input
  9. Updating the spreadsheet
  10. Destroying the applet when finished.

Now that you have completed the environmental model for your applet, you can go on to more detailed modeling. Here's where you translate the event list into the actual data flow between objects. The data flow should include input and output to key objects. The type of data models you use will depend primarily on the type and complexity of the applet you are developing.

The Planning Phase

In the planning phase, you take the requirements and specifications developed in the previous phases and determine the steps necessary to complete the project and how long each step will take. Use the resulting schedule to define the necessary milestones, goals, and time allocations to take the project through to completion.

The plans for a small project could be basic-for example, a list of 15 steps with deadlines for completion of each step. In advanced projects, there could be hundreds of project steps, with multiple steps being performed simultaneously, or a handful of steps, with each step being performed one after the other. Some steps would be dependent on other steps, meaning they could not be started until certain aspects of the project were completed. Other steps would not be dependent on any other steps and could be performed at any time during the project's development.

The planning phase can be a reality check for project constraints and requirements. For example, after you plan each step of the project you discover that it will take at least five weeks to complete the project, yet management's deadline for project completion is 30 days. In this case, you may have to renegotiate the deadline, hire additional team members, or eliminate certain time-intensive parts of the project.

Project Design

You design the layout of the project in detail during the design phase. Because applets include a common set of objects as well as unique objects, applet design is slightly different from application design. In most applets, the primary frame and associated objects are initialized in the init() method and drawn on the frame using the paint()method. Before the applet can be used, it must be started using the start() method. You can use threads if you use the run() method.

When the applet frame must be updated, the update()method is usually called. When users move to a different page or otherwise terminate the applet, the applet must be stopped and cleaned out of memory. The stop()method is responsible for stopping the applet, and the destroy()method is responsible for cleaning up after the applet.

Although most of these common objects can be called implicitly by the browser that displays the applet, you still should determine how these objects will be used and called by your applet. After you have identified the common objects, reexamine the primary frame components and event list developed in the specification phase. Each frame component and event should translate into an object you will need in the finished applet.

These objects can be grouped into object categories, such as functions and user interfaces. The user interface enables users to manipulate the primary frame of the applet. For applets, the primary frame and the user interface are usually created in the init() method or by methods that the init() method calls. Both the primary frame and the user interface should have attributes that make them easy to use, read, and understand.

As you design the primary frame and user interface in detail, keep the following attributes in mind:

Also keep in mind the main objects used on frame, which were identified in the specification phase. For the SpreadSheet applet, key objects on the primary frame include a title bar, an input area, and a display area. Following the top-down methodology of object-oriented design, you should now create a high-level design for each of these objects.

The SpreadSheet applet's title bar object is quite simple. It accepts an input parameter for the font type used to display the title text and displays the title centered on the frame.

The input area enables the user to enter new data for the current cell, which implies that you need a method to determine the current cell. Although the method used to display the input area is not complex, everything that happens after the user enters data into the input area is. At this stage, you should determine the data types the input area accepts and how the applet will determine the data type.

Generally, spreadsheets accept three types of data: labels, values, and formulas. Because the SpreadSheet applet is made for viewing on the Web, it accepts a fourth data type as well: URLs. The SpreadSheet applet determines the data type by a single character that proceeds the input value: Values are identified with the letter v, formulas with the letter f, labels with the letter l, and URLs with the letter u.

You will need objects to process the input. The main object should parse the input values and perform specific actions related to the data type. At this point, you do not have to determine these actions; however, you should note that you will need at least one object to handle each data type.

The most complex objects for data types are those that process formulas. To get a better understanding of how the applet will work, you might want to break down the formula object or at least determine the types of formulas the object will process. The SpreadSheet applet accepts formulas that use addition, subtraction, multiplication, and division. When the applet has finished processing the formula, you will need a method to recalculate values for cells that use formulas.

The next object you should examine is the display area. It contains the rows and columns of the spreadsheet, which are made up of individual cells. You will need a method to create the cells. To determine how many cells to create and ultimately how many rows and columns the spreadsheet should have, either initialize the spreadsheet to a default size or let the spreadsheet size be updated as needed.

One way to allow the spreadsheet to be updated is to accept parameter values. The SpreadSheet applet accepts parameters for font size, base font type, number of columns, number of rows, and initial values for cells. Because the user can update data on the spreadsheet, you also need a method to update the cells. Each of these parameter values will need an associated container that holds its values in the applet.

Now that you have identified the major objects used in the applet, you can create a list of these objects. As you implement each object in the next phase, you can check the object off the list to be sure that you account for all the major functions of the applet. Here's the list:

After determining most of the objects for the applet, you can go on to more detailed design. Here you should work out the interaction between objects and determine the arguments that objects will accept and pass. When you are comfortable with the design of the applet and can visualize the flow of data from object to object, you can begin the implementation phase. The next section provides a detailed look at how the SpreadSheet applet is transformed from a concept to a completed applet.

The Implementation Phase

The implementation phase is when you actually create the project you have developed. This phase is often the longest and usually involves teams of programmers who implement and integrate the source code. Usually, each programming team is responsible for creating a specific set of objects with related functionality, such as the user interface or file I/O. After these object sets are created, they are integrated into the applet.

In implementing the applet, start with the top-level objects, which usually relate to the user interface and the display, and work toward the lowest-level objects, which usually perform the key functions that the applet requires. The sections that follow demonstrate how to build the SpreadSheet applet.

Containers for the Objects Used in the SpreadSheet Applet

Containers create instances of the objects identified in the design phase so the objects can be used throughout the applet. The containers used in most applets include the low-level objects used as placeholders for parameter values. To create a container, you use instance variables. Most instance variable declarations follow the main class declaration.

The containers used in the SpreadSheet applet are shown in Listing 20.1. As you can see, there is a container for each of the parameter values identified in the design phase. Other containers hold values for key objects in the spreadsheet. Some of these objects define spreadsheet attributes with initial values.


Listing 20.1. Initial containers for the SpreadSheet applet.
public class SpreadSheet extends Applet {
    //container to hold the parameter value for title text
    String              title;  
    //container to hold the parameter value for base font
    String              bfont;  
    //container to hold the parameter value for title font
    String              tfont;  
    //container for the inputfont object
    Font                inputFont;  
    //container for the title font object
    Font                titleFont;  
    //container for the cell color object
    Color              cellColor;  
    //container for the input area's color object
    Color              inputColor;  
    //container for the parameter value for font size
    int                 fontSize;  
    //container for the cell width object
    int                 cellWidth;  
    //container for the cell height object
    int                 cellHeight;  
    //container for the title height object
    int                 titleHeight;  
    //container for the row label width object
    int                 rowLabelWidth;  
    //container for the status object
    boolean             isStopped = false;  
    //container for the update object
    boolean             fullUpdate = true;  
    //container for the rows object
    int                 rows;  
    //container for the columns object
    int                 columns;  
    //container for the current key object
    int                 currentKey = -1;  
    //container for the selected row object
    int                 selectedRow = -1;  
    //container for the selected column object
    int                 selectedColumn = -1;  
    //container for the input area
    SpreadSheetInput    inputArea;  
    //container for the individual cells
    Cell                cells[][];  
    //container for the current cell
    Cell                current = null;

Initializing the Primary Frame

Before you implement the primary frame of an applet, you should carefully consider the type and style of the document with which the applet will be used. If you do not already have an HTML document in which you want to display the applet, you should design one. Depending on the type and style of the HTML document, you might want to make some changes to the design of your applet's interface. You also might want to place additional restrictions on the size of the applet.

For example, if your applet will be used on the corporate home page, you might want to mesh the style of the applet with the existing style of the home page or modify the style of both the applet and the home page so they work well together. Most conflicts between the home page style and the applet style relate to

As you add features to the applet's frame window, keep these concepts in mind. Key features for the SpreadSheet applet's primary frame, identified in the design phase, are a title bar, an input area, and a display area. Earlier, you should have also identified key attributes for the primary frame, such as the initial frame size, the color of the frame and borders, and the style of the title bar.

Figure 20.2 shows how the SpreadSheet applet looks when completed and displayed in an HTML document. By the end of this chapter, you will be able to create this document.

Figure 20.2 : The main screen of the SpreadSheet applet.

The SpreadSheet applet's primary frame is initialized in the init() method. Because the SpreadSheet applet's init() method is fairly complex, it is broken down here into a series of steps.

Step 1: Initializing Values

The first step is to initialize containers for the necessary objects. As shown in Listing 20.2, two local containers are used for the method. The first container, rs, is a multipurpose read string holding parameter values. The second, fs, is a string used to hold the value for the font size parameter. The init() method is synchronized to prevent more than one thread from executing the method at the same time. In this way, the entire method is locked when it is being used by a thread.

After initializing the containers, initialize the color of active cells to white and set the color of the input area to blue. You can change this color value to anything you like. For example, to change the color value to yellow, use the following:

cellColor = Color.yellow;

Tip
Although you could use a parameter value to set the color for the active cell and the input area, keep in mind that Color is an object. You would have to convert the string you obtain using the getParameter() method to an instance of the Color object.


Listing 20.2. The first section of the init() method.
public synchronized void init() {
    String rs;
    String fs;

    cellColor = Color.white;
    inputColor = new Color(100, 100, 225);

Step 2: Using Parameter Values

The getParameter() method is used to read values for key attributes of the spreadsheet set in the <PARAM> tag of the applet's HTML document. If no parameter value is set by the publisher, a default value is used. Similarly, your applets should set default values for all input parameters.

As shown in Listing 20.3, the first parameter the SpreadSheet applet checks for is the font size. If no font size is set in the <PARAM> tag, it is set to 10. If the <PARAM> tag contains a setting for the font size, the string read with the getParameter() method is converted to an integer using the Integer.parseInt() method, which you will use to convert all parameter values that you plan to manipulate as numbers.

The fontSize parameter value is used to set key spreadsheet attributes proportional to the font size. Keeping the size of objects used in the spreadsheet proportional to the current font size is essential to the readability and usability of the applet. The current font size affects every major display aspect of the spreadsheet, including the height and width of cells in the spreadsheet, the width of row labels, the height of the area used for titles, the font size used for titles, the position of the title, the position of row and column headers, the size of the input area, and the position of text in the input area.

For this reason, throughout the SpreadSheet applet, you will see the size of objects set proportional to the fontSize object. You can use this technique in your applets to keep the display consistent with the current font size.

The next parameter values the SpreadSheet applet checks for are the base font type and the title font type. Using these values, initialize the inputFont and titleFont objects. The default value for both font types, Courier, is used when no parameter value is set in the <PARAM> tag. When selecting a default font type, you should use one that is widely available on most systems, such as Courier, Helvetica, Times Roman, or System.

The applet also checks for a title for the spreadsheet. If no parameter value is set, the title is set to Spreadsheet.

The final parameter values the applet checks for are the number of rows and columns to be used in the spreadsheet. These must be integer values, so the string read using the getParameter() method is converted to an integer using the parseInt() method.


Listing 20.3. Getting and using parameter values.
    fs = getParameter("fontsize");
    if (fs == null) {
        fontSize = 10;
    } else {
        fontSize = Integer.parseInt(fs);
    }

cellWidth = fontSize * 10;
cellHeight = fontSize * 2;
titleHeight = fontSize * 2;
rowLabelWidth = fontSize * 3;
    bfont = getParameter("basefont");
    if (bfont == null) {
        bfont = "Courier";
    }
    inputFont = new Font(bfont, Font.PLAIN, fontSize);

    tfont = getParameter("titlefont");
    if (tfont == null) {
        tfont = "Courier";
    }
    titleFont = new Font(tfont, Font.BOLD, (fontSize + 2));

    title = getParameter("title");
    if (title == null) {
        title = "Spreadsheet";
    }
    rs = getParameter("rows");
    if (rs == null) {
        rows = 9;
    } else {
        rows = Integer.parseInt(rs);
    }
    rs = getParameter("columns");
    if (rs == null) {
        columns = 5;
    } else {
        columns = Integer.parseInt(rs);
    }

Step 3: Initializing the Spreadsheet

Initializing an object as complex as a spreadsheet requires careful forethought, especially when its size can be dynamically updated using parameter values. To reduce the difficulty of this task, you could set the spreadsheet to a predetermined size, such as nine rows and five columns. Then, after you understand the way the spreadsheet is built, you could modify the code to allow the size of the spreadsheet to be updated as needed.

To create the spreadsheet, determine the number of cells needed. Then initialize the cells to default values and attributes. One way to handle this is to build an array of an array. The first array holds the row index. The second array holds the column index. Together, these indexes point to a particular cell in the spreadsheet.

To make it easier to set initial values for each cell using parameter values, the spreadsheet applet assumes each column is ordered alphabetically and each row is ordered numerically. Thus, the index value a1 points to the cell in column 1, row 1, and the value b4 points to the cell in column 2, row 4. Because Java integers begin with 0, you might be wondering what happened to values for column 0. All cells in column 0 are used to hold values for row headers. Similarly, all cells in row 0 are used to hold values for column headers. This way, when you create a spreadsheet with the parameter value for rows set to 3 and columns set to 4, you get a spreadsheet with 3 rows and 4 columns.

As shown in Listing 20.4, reading the parameter values for cells from the HTML document is accomplished with two conditional loops. The first loop continues until the row count is reached, and the second continues until the column count is reached. Within the second loop, the index values for the row and column are used to build an array of arrays.

Each new cell is created as an instance of the Cell object. Included in each new cell are parameters for the background color of the cell; the color of text in the cell; the width and height of the cell; an array containing the cell's index, such as a1 or b4; and a value associated with the cell as read by the getParameter() method. Following the logic of the cell-building loops, the following parameter values could be used:

<PARAM NAME=a1 value="lWholesale">
<PARAM NAME=a2 value="v20195">
<PARAM NAME=a3 value="v7280">
<PARAM NAME=a4 value="v6127">
<PARAM NAME=a5 value="v9803">
<PARAM NAME=a6 value="v4150">
<PARAM NAME=a7 value="fA2+(A3/A4)">

If you do not set a value for a particular cell in the spreadsheet, the setUnparsedValue() method is called to build a value for the Cell object. Just as the setUnparsedValue() method is called to add default values for undefined cells, the cell values provided to the Cell object are parsed as well. This parsing takes place in the Cell class.


Listing 20.4. Initializing the spreadsheet.
cells = new Cell[rows][columns];
char l[] = new char[1];
for (int i=0; i < rows; i++) {
    for (int j=0; j < columns; j++) {

        cells[i][j] = new Cell(this,
                               Color.lightGray,
                               Color.black,
                               cellColor,
                               cellWidth - (fontSize/4),
                               cellHeight - (fontSize/4));
        l[0] = (char)((int)'a' + j);
        rs = getParameter("" + new String(l) + (i+1));
        if (rs != null) {
            cells[i][j].setUnparsedValue(rs);
        }
    }
}

Step 4: Sizing the Spreadsheet

Sizing a complex structure on the applet frame is a difficult task. You do not want your key objects to be larger than the frame or placed off the frame and thus off the screen. However, you do want the objects to fill the frame if possible. The best way to build the structure within your frame is to base it on the actual dimensions of the frame by creating a dimension object with the current size values and then allocating this area proportionally to each object as necessary.

Although this is the best way to size the objects, you do not always have this luxury and may have to size objects on the basis of some other constraint. The font size is a constraint in the SpreadSheet applet simply because you must be able to view the data in the cells no matter the font size.

The inputArea for the SpreadSheet applet is created as a new instance of the SpreadSheetInput object. The size of the inputArea is based on the current width minus two pixels, and its height is based on the current height minus two pixels.

The spreadsheet is then resized on the basis of the width and height of all component objects of the spreadsheet. As noted earlier, these objects are sized proportional to the fontSize object. The code used to initialize the input area and size the spreadsheet is shown in Listing 20.5.


Listing 20.5. Sizing the spreadsheet.
    Dimension d = size();
    inputArea = new SpreadSheetInput(null, this, d.width - 2, cellHeight - 2,
                                     inputColor, Color.white);
    resize(columns * cellWidth + rowLabelWidth,
           ((rows + 1) * cellHeight) + cellHeight + titleHeight);
}

Determining the State of the Applet

After initializing the applet, implement the other common applet methods if necessary. Usually, your applet does not have to explicitly call the start(), stop(), and destroy() methods. You can better determine the behavior of your applet if you define these methods.

The SpreadSheet applet uses the start() and stop() methods to determine the state of the applet. When the applet is active, the boolean value isStopped is set to false; when inactive, the boolean value isStopped is set to true. The value isStopped is used later in the applet to stop it from repainting itself before exiting.

The destroy method is used to clean up after you halt the applet. If you have processes that could be active even after the applet exits, terminate them in your applet's destroy() method. Examples include any connections you made to remote hosts or documents you were accessing.

The start(), stop(), and destroy() methods for the SpreadSheet applet are shown in Listing 20.6.


Listing 20.6. The state of the applet.
public void start() {
    isStopped = false;
}
public void stop() {
    isStopped = true;
}
public void destroy() {
    for (int i=0; i < rows; i++) {
        for (int j=0; j < columns; j++) {
            if (cells[i][j].type == Cell.URL) {
                cells[i][j].updaterThread.stop();
            }
        }
    }
}

Running Applet Threads

The task of running applet threads is handled by the run() method. As shown in Listing 20.7, the run() method for the SpreadSheet applet is fairly lengthy and is handled in a separate class called CellUpdater that extends the Thread class. By extending the Thread class, the applet can use Java's multithreading capabilities to accept input from the token stream and display the data in the spreadsheet. This allows cells of the spreadsheet to be dynamically updated based on data coming from an input stream.

The function of the while loop in the CellUpdater class is to read string tokens passed from the input stream. As long as there are tokens to read, the run() method continues through the while loop. This causes the applet to call the setTransientValue method, which in turn sets a transient value flag. Before continuing the loop, the repaint() method is called to update the frame. When there are no more tokens to read, the dataStream object is closed and the run() method waits for the next token to be passed.

To use this threading feature of the SpreadSheet applet, define a cell as a URL type and specify the URL path to the document you want to read on the input stream.


Listing 20.7. Running and updating the applet.
class CellUpdater extends Thread {

    Cell        target;
    InputStream dataStream = null;
    StreamTokenizer tokenStream;

    public CellUpdater(Cell c) {
        super("cell updater");
        target = c;
    }

    public void run() {

        try {
            dataStream = new URL(target.app.getDocumentBase(),
                                 target.getValueString()).openStream();
            tokenStream = new StreamTokenizer(dataStream);
            tokenStream.eolIsSignificant(false);

            while (true) {
                switch (tokenStream.nextToken()) {
                case tokenStream.TT_EOF:
                    dataStream.close();
                    return;
                default:
                    break;
                case tokenStream.TT_NUMBER:
                    target.setTransientValue((float)tokenStream.nval);
                    if (! target.app.isStopped && ! target.paused) {
                        target.app.repaint();
                    }
                    break;
                }
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        } catch (IOException e) {
            return;
        }
    }
}

Painting the Frame

After you have initialized the spreadsheet, you must display it. This is usually done by means of the paint() method, which draws all the objects added during the initialization phase. As discussed in "Step 4: Sizing the Spreadsheet," the objects should be sized according to the dimensions of the frame or a key constraint such as the font size. You should also paint the objects in the order they are to be added to the frame.

The SpreadSheet applet uses many interesting techniques to place objects precisely on the frame. As shown in Listing 20.8, centering the title horizontally in the title area involves five steps:

  1. Obtaining the dimension of the frame
  2. Determining the pixel length of the title on the basis of the current font
  3. Subtracting the length of the title from the width of the frame
  4. Adding in a factor that accounts for differences in font size
  5. Dividing the result by 2

The value you obtain by doing this is the x coordinate for the beginning of the title. You also need a y coordinate for the title. Because the height of the title area is relative to the font size used, the font size is used to help determine this coordinate.

Next, the paint() method draws the input area by simply filling a rectangle with a color based on the value of the inputColor object. The starting location for the upper-left corner of the input area is determined by moving vertically down the height of one cell. The width of the input area is set to the width of the frame, and the height is set to the height of one cell.

Finally, the paint() method draws the individual cells. To create them, use a series of draws. The first adds the horizontal lines for rows using the draw3DRect method of the Graphics class. As the 3D lines are drawn to the frame, blue numerals representing the row numbers are added as appropriate. The second draw adds the vertical lines for columns, again using the draw3DRect method. As the 3D lines are drawn to the frame, red letters representing the column letters are added as appropriate. Finally, data is added to the cells by painting the values associated with a cell precisely within the rectangles created by the previous draws.

The last section of code in the paint() method, although only a few lines, is important to the spreadsheet. The line of code using the draw3DRect() method draws a 3D line above the input area and on the left side of the frame. The next-to-last line calls the paint() method of the inputArea class, which ensures that the data associated with the currently selected cell is painted to the input area.


Listing 20.8. Painting the frame.
public synchronized void paint(Graphics g) {

    int i, j;
    int cx, cy;
    char l[] = new char[1];

    Dimension d = size();

    //draws the title on the frame
    g.setFont(titleFont);
    i = g.getFontMetrics().stringWidth(title);
    g.drawString((title == null) ? "Spreadsheet" : title,
                 (d.width - i + (fontSize*3/2)) / 2, (fontSize*3/2));

    //draws the input area on the frame
    g.setColor(inputColor);
    g.fillRect(0, cellHeight, d.width, cellHeight);

    //draws the lines for rows using 3d effect
    //adds row numbering in blue
    g.setFont(titleFont);
    for (i=0; i < rows+1; i++) {
        cy = (i+2) * cellHeight;
        g.setColor(getBackground());
        g.draw3DRect(0, cy, d.width, 2, true);
        if (i < rows) {
            g.setColor(Color.blue);
            g.drawString("" + (i+1), fontSize, cy + (fontSize *3/2));
        }
    }

    //draws the lines for columns using 3d effect
    //adds column alphas in red
    g.setColor(Color.red);
    for (i=0; i < columns; i++) {
        cx = i * cellWidth;
        g.setColor(getBackground());
        g.draw3DRect(cx + rowLabelWidth,
                      2 * cellHeight, 1, d.height, true);
        if (i < columns) {
            g.setColor(Color.red);
            l[0] = (char)((int)'A' + i);
            g.drawString(new String(l),
                         cx + rowLabelWidth + (cellWidth / 2),
                         d.height - 3);
       }
    }

    //paints the cell data
    for (i=0; i < rows; i++) {
        for (j=0; j < columns; j++) {
            cx = (j * cellWidth) + 2 + rowLabelWidth;
            cy = ((i+1) * cellHeight) + 2 + titleHeight;
            if (cells[i][j] != null) {
                cells[i][j].paint(g, cx, cy);
            }
        }
    }

    g.setColor(getBackground());
   //draws a 3d line for the input area and to the left side of the frame
    g.draw3DRect(0, titleHeight, d.width, d.height - titleHeight, false);
    //paints the data of the current cell to the input area
    inputArea.paint(g, 1, titleHeight + 1);
}

Updating the Applet's Frame

When changes occur in the applet, you must repaint the applet's frame using the repaint() method of the Applet class. If you recall earlier discussions on repainting the applet's frame, you probably know that the repaint() method calls the update() method of the Applet class, which in turn clears the screen and calls the paint() method. The paint method then draws in the applet's frame.

Clearing and then drawing the frame produces a noticeable flicker. To reduce it, usually it's best to override the update() method of the Applet class. You do this by defining an update() method in your applet that does not clear the frame at all before painting it and, if possible, clears only the parts of the screen that have changed.

Listing 20.9 shows the update() method for the SpreadSheet applet. This method uses a boolean value called full update to determine whether to repaint the whole frame or just a portion of it. When only a portion must be repainted, the appropriate section is redrawn. The partial update occurs when the user selects a cell and the applet places the data associated with the cell in the input area and redraws the cell with a white background. When the entire applet frame needs to be updated, the update() method calls the paint() method directly without clearing the frame first.


Listing 20.9. Updating the frame.
public void update(Graphics g) {
    if (! fullUpdate) {
        int cx, cy;

        g.setFont(titleFont);
        for (int i=0; i < rows; i++) {
            for (int j=0; j < columns; j++) {
                if (cells[i][j].needRedisplay) {
                    cx = (j * cellWidth) + (fontSize*2) + rowLabelWidth;
                    cy = ((i+1) * cellHeight) + (fontSize*2) + titleHeight;
                    cells[i][j].paint(g, cx, cy);
                 }
            }
        }
    } else {
        paint(g);
        fullUpdate = false;
    }
}

Handling Events in the Applet

Updates to the SpreadSheet applet are driven by the occurrence of user events, such as keypresses and mouse button clicks. As discussed in previous chapters, events are usually handled with either the action() method or the handleEvent() method. The action() method of the Components class is the preferred way to handle a limited number of events. The handleEvent method of the Event class is the preferred way to handle multiple or complex series of events.

When an applet has only a few possible events that deal with the mouse or keypresses, you can call more direct methods that eliminate the necessity of nested if or case statements to check event type. These methods, like the action() method, are members of the Component class. Table 20.1 lists these events.

Table 20.1. Direct event handling with Component class methods.
MethodDescription Called with Values
keyDown() Called if a key is pressedkeyDown(Event evt, int x)
keyUp() Called if a key is releasedkeyUp(Event evt, int x)
mouseDown() Called if the mouse button is downmouseDown(Event evt, int x, int y)
mouseDrag() Called if the mouse moves while a button is pressed mouseDrag(Event evt, int x, int y)
mouseEnter() Called when the mouse enters the component mouseEnter(Event evt, int x, int y)
mouseExit() Called when the mouse exits the component mouseExit(Event evt, int x, int y)
mouseMove() Called if the mouse moves while no buttons are pressed mouseMove(Event evt, int x, int y)
mouseUp() Called if the mouse button is upmouseUp(Event evt, int x, int y)

The SpreadSheet applet uses two of these direct methods to check for events: mouseDown and keyDown. The handling of these events is shown in Listing 20.10.

When a user moves the mouse pointer over a cell and selects it by clicking the mouse button, the applet determines the exact cell the user selected based on the position of the mouse in the spreadsheet. As you can see from Listing 20.10, finding the selected cell is a lot of work.

The first conditional loop ensures that the y coordinate of the mouse pointer is in a valid section of the spreadsheet. If the y coordinate is less than the combined height of the title and input area, the selected row is set to -1. The nested if loop sets the value for the current cell to null when the user clicks the mouse button when the pointer is in the title area and the deselect method is called.

The second conditional loop ensures that the x coordinate of the mouse pointer is in a valid section of the spreadsheet. If the x coordinate is less than the width of the row label, the selected row is set to -1. The nested if loop sets the value for the current cell to null when the user clicks the mouse button when the pointer is in the row label area and the deselect method is called.

Next, the method determines the column/row coordinate for the selected cell. If the selected cell is greater than the number of rows or columns in the spreadsheet, the value for the current cell is set to null and the deselect method is called. If the column/row coordinate for the selected cell is valid, the value of the selected cell is displayed in the input area and the select() method is called to paint the current cell with a white background.

The final event checked for is a keypress. As long as a valid cell is selected, any character key you press on the keyboard is displayed in the input area. When you press Enter and a valid cell is selected, the value from the input area replaces the value of the current cell and the entire spreadsheet is updated. This is all handled by setting fullUpdate to true and calling the keyDown() method, which actually does the work of updating the spreadsheet as necessary.

You can use similar techniques to determine the selected object in your applets as well. The key is to narrow the valid range by eliminating areas of the frame that cannot be selected, such as the title bar, rows with header labels, and columns with header labels. When you narrow the selection area, you can determine the specific object the user selected.


Listing 20.10. Handling applet events.
public boolean mouseDown(Event evt, int x, int y) {
        Cell cell;

        //ensures the y coordinate of the mouse pointer
        //is not in the title/input area
        if (y < (titleHeight + cellHeight)) {
            selectedRow = -1;
            if (y <= titleHeight && current != null) {
                current.deselect();
                current = null;
            }
            return true;
        }
        //ensures the x coordinate of the mouse pointer
        //is not in the row label area
        if (x < rowLabelWidth) {
            selectedRow = -1;
            if (current != null) {
                current.deselect();
                current = null;
            }
            return true;
        }

        //determines the row of the selected cell
        selectedRow = ((y - cellHeight - titleHeight) / cellHeight);

        //determines the column of the selected cell
        selectedColumn = (x - rowLabelWidth) / cellWidth;

        //ensures the row and column coordinate is valid
        if (selectedRow >= rows ||
            selectedColumn >= columns) {
            selectedRow = -1;
            if (current != null) {
                current.deselect();
                current = null;
            }

        //if the row-column coordinate is valid place the selected cell's value
        //in the input area
        } else {
            cell = cells[selectedRow][selectedColumn];
            inputArea.setText(new String(cell.getPrintString()));
            if (current != null) {
                current.deselect();
            }
            current = cell;
            current.select();
            requestFocus();
            fullUpdate = true;
            repaint();
        }
        return true;
    }

    //determine if a key is pressed
    public boolean keyDown(Event evt, int key) {
        fullUpdate=true;
        inputArea.keyDown(key);
        return true;
    }
}

Updating the Current Cell's Value

User events drive changes in the spreadsheet. When a cell's value is changed, the spreadsheet must be updated to reflect this. As noted in the "Handling Events in the Applet" section in this chapter, the keyDown() method is called to handle the update. One of the high-level objects keyDown calls is the setCurrentValue() method of the SpreadSheet class, which takes the current value of the input area and assigns it to the active cell.

Two versions of the setCurrentValue() method are defined in the applet: The first sets values for floating-point numbers, and the second sets values for strings. This is a good example of overloading a method to handle multiple types of input. Listing 20.11 shows the code for updating the current cell's value.


Listing 20.11. Updating the current cell's value.
public void setCurrentValue(float val) {
    if (selectedRow == -1 || selectedColumn == -1) {
        return;
    }
    cells[selectedRow][selectedColumn].setValue(val);
    repaint();
}

public void setCurrentValue(int type, String val) {
    if (selectedRow == -1 || selectedColumn == -1) {
        return;
    }
    cells[selectedRow][selectedColumn].setValue(type, val);
    repaint();
}

Handling Formulas

The SpreadSheet class is responsible for most of the high-level functions of the applet. To complete this class, you need methods for calculating formulas and evaluating formulas.

Before the spreadsheet is displayed, the recalculate() method is called to convert any formulas the spreadsheet may have to numeric values. Without a call to this method, the spreadsheet makes no calculations, and the formula is displayed in the cell instead of the numeric value. To ensure that both the numeric value and the formula are available to the user, the SpreadSheet applet stores the formula and the value in separate buffers.

As you can see from Listing 20.12, the recalculate() method goes through each cell in the spreadsheet checking for the cell type FORMULA. If a cell contains a formula, the formula is evaluated using the evaluateFormula() method. The values returned from the evaluateFormula() method are passed to the setRawValue() method.


Listing 20.12. Calculating formula values.
public void recalculate() {
    int     i,j;

    //System.out.println("SpreadSheet.recalculate");
    for (i=0; i < rows; i++) {
        for (j=0; j < columns; j++) {
            if (cells[i][j] != null && cells[i][j].type == Cell.FORMULA) {
                cells[i][j].setRawValue(evaluateFormula(cells[i][j].parseRoot));
                cells[i][j].needRedisplay = true;
            }
        }
    }
    repaint();
}

The SpreadSheet applet has the capability to parse formulas with basic math functions like addition, subtraction, multiplication, and division by examining formula nodes. Nodes are defined in the applet as class-level objects that have a left value, an operand, and a right value.

The evaluateFormula() method shown in Listing 20.13 examines formula nodes looking for operands, values, and cell references. Many computer science majors will recognize the simple algorithm used to evaluate spreadsheet formulas as an expression tree. A simple recursive process is then used to evaluate the nodes of the tree. Values associated with these node components are returned to the setRawValue() method, which displays the numeric value of the cell as a string.

Tip
Recursion on trees is one of the basic algorithms in the programmer's toolkit. If this is the first time you have encountered an expression tree algorithm, take the time to learn how this is done. Start by examining the evaluateFormula() method and then examine the parseFormula() and parseValue() methods of the Cell class. You may be surprised at the number of uses for this algorithm.


Listing 20.13. Evaluating formulas.
public float evaluateFormula(Node n) {
    float   val = 0.0f;

    if (n == null) {
        return val;
    }
    switch (n.type) {
      case Node.OP:
        val = evaluateFormula(n.left);
        switch (n.op) {
          case '+':
            val += evaluateFormula(n.right);
            break;
          case '*':
            val *= evaluateFormula(n.right);
            break;
          case '-':
            val -= evaluateFormula(n.right);
            break;
          case '/':
            val /= evaluateFormula(n.right);
            break;
        }
        break;
      case Node.VALUE:
        return n.value;
      case Node.CELL:
        if (n == null) {
        } else {
            if (cells[n.row][n.column] == null) {
            } else {
                return cells[n.row][n.column].value;
            }
        }
    }

    return val;
}

Creating Cells

The next class-level object in the spreadsheet is the Cell object, which is responsible for maintaining the attributes of a cell. Listing 20.14 shows the declarations for the Cell object's containers.

When new instances of the Cell class are created during the initialization of the frame, the Cell method is called and the initial attributes for the cell are set. These attributes include the background color, the foreground color, the color to use when the cell is selected, and the width and height of the cell. Other attributes of the cell are set to default values or are based on the value
associated with each cell as read by the getParameter() method.


Listing 20.14. Containers for the Cell object.
class Cell {

    public static final int VALUE = 0;   //initializes the VALUE container
    public static final int LABEL = 1;   //initializes the LABEL container
    public static final int URL   = 2;   //initializes the URL container
    public static final int FORMULA = 3;   //initializes the FORMULA container
    Node        parseRoot;   //sets the node for the cell
    boolean     needRedisplay;   //used to determine the need for redisplay
    boolean selected = false;    //used to determine whether cell is selected
    boolean transientValue = false;   //tracks need to set transient value
    public int  type = Cell.VALUE;    //sets the cell type
    String      valueString = "";    //initializes the value string
    String      printString = "v";   //initiliazes the print string
    float       value;   //container for a float called value
    Color       bgColor;   //container for the background color
    Color       fgColor;   //container for the foreground color
    Color       highlightColor;   //container for cell color when selected
    int         width;   //container for the width of the cell
    int         height;   //container for the height of the cell
    SpreadSheet app;    //container for the SpreadSheet object
    CellUpdater updaterThread;   //container for the CellUpdater object
    boolean     paused = false;    //used to determine if paused

Preparing Cells for Display

After the containers for Cell objects are initialized, a series of methods, shown in Listing 20.15, prepare the cell for display.

The Cell() method is called with values for the key attributes of the cell; it then associates these values with the Cell object. All cells of the spreadsheet must have a type, valueString, and printString associated with them. The first character of the value read by the getParameter() method character is used to set the cell type. The valueString excludes the first character and is used in calculations. The printString contains the value of the cell as it should be displayed to the input area when the cell is selected.

The setUnparsedValue() method sets the cell type. If the first character is a v, the type is set to VALUE; if it is an l, the type is set to LABEL; if it is a u, the type is set to URL; and if it is an f, the type is set to FORMULA.

setValue()is an overloaded method that builds the valueString and printString objects of all cells. The first version of the method accepts string values and, depending on cell type, calls associated methods. The second version accepts floating-point values and is called only when the cell type is VALUE. This overloaded method in turn calls the setRawValue() method that converts the floating-point value it is passed to a String object.

The setTransientValue()method sets a transient value associated with the cell. This value is set only when a number is passed through the token stream.


Listing 20.15. Preparing the cell for display.
//sets attributes for the Cell objects as passed
public Cell(SpreadSheet app,
            Color bgColor,
            Color fgColor,
            Color highlightColor,
            int width,
            int height) {
    this.app = app;
    this.bgColor = bgColor;
    this.fgColor = fgColor;
    this.highlightColor = highlightColor;
     this.width = width;
       this.height = height;
       needRedisplay = true;
   }

   //sets the valueString by converting the numeric value of the cell to a string
   public void setRawValue(float f) {
       valueString = Float.toString(f);
       value = f;
   }

   //sets the cell type for values, formulas, labels and URLs
   public void setUnparsedValue(String s) {
       switch (s.charAt(0)) {
         case 'v':
           setValue(Cell.VALUE, s.substring(1));
           break;
         case 'f':
           setValue(Cell.FORMULA, s.substring(1));
           break;
         case 'l':
           setValue(Cell.LABEL, s.substring(1));
           break;
         case 'u':
           setValue(Cell.URL, s.substring(1));
           break;
       }
}

   //sets the printString by prepending a type indicator to the valueString
   public void setValue(float f) {
       setRawValue(f);
       printString = "v" + valueString;
       type = Cell.VALUE;
       paused = false;
       app.recalculate();
       needRedisplay = true;
   }

   //sets the printString by prepending a type indicator to the valueString
   public void setValue(int type, String s) {

       paused = false;
       if (this.type == Cell.URL) {
           updaterThread.stop();
           updaterThread = null;
       }

       valueString = new String(s);
       this.type = type;
       needRedisplay = true;
       switch (type) {
         case Cell.VALUE:
           setValue(Float.valueOf(s).floatValue());
           break;
         case Cell.LABEL:
           printString = "l" + valueString;
           break;
         case Cell.URL:
           printString = "u" + valueString;
           updaterThread = new CellUpdater(this);
           updaterThread.start();
           break;
         case Cell.FORMULA:
           parseFormula(valueString, parseRoot = new Node());
           printString = "f" + valueString;
           break;
       }
       app.recalculate();
   }

   //sets a transient value flag
   public void setTransientValue(float f) {
       transientValue = true;
       value = f;
       needRedisplay = true;
       app.recalculate();
   }

Utility Methods

Four utility methods, shown in Listing 20.16, handle low-level tasks for Cell objects: The getPrintString()method returns the value of the printString associated with the cell; the getValueString()method returns the value of the valueString associated with the cell; the select()method sets flags to highlight the cell when the frame is next painted; and the deselect() method sets flags to reset the cell's background to its original color when the frame is next painted.


Listing 20.16. Utility methods of the Cell class.
public String getPrintString() {
    return printString;
}
public String getValueString() {
    return valueString;
}
public void select() {
    selected = true;
    paused = true;
}
public void deselect() {
    selected = false;
    paused = false;
    needRedisplay = true;
}

Parsing Formulas

Parsing formulas associated with a cell is a difficult task handled by two methods: parseFormula and parseValue. These two methods take cell formulas apart piece by piece by examining formula nodes. As you examine Listing 20.17, study the algorithm that builds and dissects the nodes. Recall that modes are defined in the applet as class-level objects that have a left value, an operand, and a right value. (See the section "Handling Formulas" for a listing of valid operands and how they are used.)

The values associated with a node can be either implicit or explicit. Explicit node values are numbers that you want to add, multiply, divide, or subtract. Implicit values are numbers associated with a particular cell. Reference implicit values in formulas using the column/row coordinate of the cell.

A node that has only a left value for it is still valid. Using these concepts of nodes, the basic syntax for formulas is

value
value operand value

where each instance of value can be an actual value or a reference to a particular cell of the spreadsheet, such as

A2 * 4

or

A2 + B2

To allow for more complex formulas, the parseFormula and parseValue methods allow you to define subformulas for the right-hand value of the original node using parentheses. Because subformulas are treated as nodes, they also can have a left value, an operand, and a right value. You can continue to nest subformulas as long as you follow the concept of nodes. Using subformulas, you can create formulas with the following syntax:

value operand (value operand value)
value operand (value operand (value operand value))

You can continue to nest subformulas as long as you follow the basic node structure of value operand value. As discussed earlier, the algorithm that makes this work is a recursive procedure on an expression tree.


Listing 20.17. Parsing formulas.
/**
 * Parse a spreadsheet formula. The syntax is defined as:
 *
 * formula -> value
 * formula -> value op value
 * value -> '(' formula ')'
 * value -> cell
 * value -> <number>
 * op -> '+' | '*' | '/' | '-'
 * cell -> <letter><number>
 */
public String parseFormula(String formula, Node node) {
    String subformula;
    String restFormula;
    float value;
    int length = formula.length();
    Node left;
    Node right;
    char op;

    if (formula == null) {
        return null;
    }
    subformula = parseValue(formula, node);
    if (subformula == null || subformula.length() == 0) {
        //System.out.println("Parse succeeded");
        return null;
    }
    if (subformula == formula) {
        return formula;
    }

    // parse an operator and then another value
    switch (op = subformula.charAt(0)) {
      case 0:
        return null;
      case ')':
        return subformula;
      case '+':
      case '*':
      case '-':
      case '/':
        restFormula = subformula.substring(1);
        subformula = parseValue(restFormula, right=new Node());
        if (subformula != restFormula) {
            left = new Node(node);
            node.left = left;
            node.right = right;
            node.op = op;
            node.type = Node.OP;
            return subformula;
        } else {
            return formula;
        }
      default:
        return formula;
    }
}
public String parseValue(String formula, Node node) {

    char    c = formula.charAt(0);
    String  subformula;
    String  restFormula;
    float   value;
    int     row;
    int     column;

    restFormula = formula;
    if (c == '(') {
        restFormula = formula.substring(1);
        subformula = parseFormula(restFormula, node);
        if (subformula == null ||
            subformula.length() == restFormula.length()) {
            return formula;
        } else if (! (subformula.charAt(0) == ')')) {
            return formula;
        }
        restFormula = subformula;
    } else if (c >= '0' && c <= '9') {
        int i;

        try {
            value = Float.valueOf(formula).floatValue();
        } catch (NumberFormatException e) {
            return formula;
        }
        for (i=0; i < formula.length(); i++) {
            c = formula.charAt(i);
            if ((c < '0' || c > '9') && c != '.') {
                break;
            }
        }
        node.type = Node.VALUE;
        node.value = value;
        restFormula = formula.substring(i);
            //                     " rest = " + restFormula);
        return restFormula;
    } else if (c >= 'A' && c <= 'Z') {
        int i;

        column = c - 'A';
        restFormula = formula.substring(1);
        row = Float.valueOf(restFormula).intValue();
        for (i=0; i < restFormula.length(); i++) {
            c = restFormula.charAt(i);
            if (c < '0' || c > '9') {
                break;
            }
        }
        node.row = row - 1;
        node.column = column;
        node.type = Node.CELL;
        if (i == restFormula.length()) {
            restFormula = null;
        } else {
            restFormula = restFormula.substring(i);
            if (restFormula.charAt(0) == 0) {
                return null;
            }
        }
    }

    return restFormula;
}

Painting Cells

The paint() method of the Cell class sets the colors for spreadsheet cells. If the selected flag is set, the background color of the cell is set to the value associated with the highlightColor object-white. If it is not set, the background color of the cell is set to the value associated with the bgColor object-gray. When the background color is set, the rectangle associated with the cell is painted.

When new cell objects are created, the fgcolor attribute is set to black. This attribute is used to set the font color for each cell. If the cell type is VALUE or LABEL, the font color is set to black; if the cell type is FORMULA, it is set to red; and if the cell type is URL, it is set to blue. When the color for data associated with a cell is set, the data is drawn to the frame using the drawString() method. Listing 20.18 shows the paint() method of the Cell class.


Listing 20.18. Painting cells.
    public void paint(Graphics g, int x, int y) {
        if (selected) {
            g.setColor(highlightColor);
        } else {
            g.setColor(bgColor);
        }
        g.fillRect(x, y, width - 1, height);
        if (valueString != null) {
            switch (type) {
              case Cell.VALUE:
              case Cell.LABEL:
                g.setColor(fgColor);
                break;
              case Cell.FORMULA:
                g.setColor(Color.red);
                break;
              case Cell.URL:
                g.setColor(Color.blue);
                break;
            }
            if (transientValue){
                g.drawString("" + value, x, y + (height / 2) + 5);
            } else {
                if (valueString.length() > 14) {
                    g.drawString(valueString.substring(0, 14),
                                 x, y + (height / 2) + 5);
                } else {
                    g.drawString(valueString, x, y + (height / 2) + 5);
                }
            }
        }
        needRedisplay = false;
    }
}

Building the Node Object

You use the Node class of the SpreadSheet applet to build the nodes used in formulas. As you can see in Listing 20.19, nodes are not complex objects, and building a node is rather easy when you understand that all nodes have a left value, an operand, and a right value associated with them.

To track the types of values associated with nodes, three static variables are used: The OP variable is set for operands, the VALUE variable is set for numeric values, and the CELL variable is set for references to cell objects.

The Node class includes an overloaded method called Node(). The first version of the Node() method builds a generic node that is used when parsing subformulas. The second version is used when parsing formulas.


Listing 20.19. Building nodes.
class Node {

    //set for node components that are operands
    public static final int OP = 0;
    //set for node components that are values
    public static final int VALUE = 1;
    //set for node components that are cell references
    public static final int CELL = 2;

    int         type;
    Node        left;   //the lefthand value for the node
    Node        right;   //the righthand value for the node
    int         row;   //the row coordinate of the selected cell
    int         column;   //the column coordinate of the selected cell
    float       value;   //the current value
    char        op;   //the operand +, -, /, *

    public Node() {
        left = null;
        right = null;
        value = 0;
        row = -1;
        column = -1;
        op = 0;
        type = Node.VALUE;
    }
    public Node(Node n) {
        left = n.left;
        right = n.right;
        value = n.value;
        row = n.row;
        column = n.column;
        op = n.op;
        type = n.type;
    }
}

Building the Input Area

The input area is the final object needed in the SpreadSheet applet. Although it could have been built using a simple textField object, a more advanced approach is to build an input area that behaves and looks exactly as you want it to.

Note
You will find that there are many times when control over the behavior and style of the input area is needed in an advanced applet. To do this, you must build your own input area.

When building your own input area, there are many things you should consider. Most are obvious and relate to why you wanted to create your own input area in the first place. Style considerations include the following:

Behavior considerations include the following:

Two class-level objects are used to build the input area for the SpreadSheet applet: SpreadSheetInput and InputField.

The SpreadSheetInput Class

The SpreadSheetInput class, shown in Listing 20.20, extends the InputField class, which is defined next. The class includes two fairly basic methods.

The SpreadSheetInput() method passes the values with which it was called to the InputField class. The selected() method calls the setCurrentValue method with the appropriate parameters so the value of the input area can be passed back to the currently selected cell. Thus, the cell is updated when the user makes a change and presses Enter.


Listing 20.20. The SpreadSheetInput class.
class SpreadSheetInput extends InputField {

    public SpreadSheetInput(String initValue,
                            SpreadSheet app,
                            int width,
                            int height,
                            Color bgColor,
                            Color fgColor) {
        super(initValue, app, width, height, bgColor, fgColor);
    }

    public void selected() {
        float f;

        switch (sval.charAt(0)) {
          case 'v':
            try {
                f = Float.valueOf(sval.substring(1)).floatValue();
                ((SpreadSheet)app).setCurrentValue(f);
            } catch (NumberFormatException e) {
                System.out.println("Not a float...");
            }
            break;
          case 'l':
            ((SpreadSheet)app).setCurrentValue(Cell.LABEL, sval.substring(1));
            break;
          case 'u':
            ((SpreadSheet)app).setCurrentValue(Cell.URL, sval.substring(1));
            break;
          case 'f':
           ((SpreadSheet)app).setCurrentValue(Cell.FORMULA, sval.substring(1));
            break;
        }
    }
}

The InputField Class

As you can see in Listing 20.21, the InputField class is more complex than the SpreadSheetInput class, primarily because the InputField object defines the style and behavior of the input area.

The first section of the InputField class initializes containers that determine the style of the input area and limit its behavior. The maximum number of characters the input area accepts is 50. Each character entered into the input area is stored in an array. When the user adds characters to the input area by either selecting a cell or entering data, the array grows. When the user deletes characters, the array shrinks.

The InputField() method is called to set the attributes for the input area. In the unlikely case that the method is called with an initial value set, the string buffer is set to this value.

The setText() method updates the input area when a cell is selected or when text is entered or deleted by reading the string buffer character by character. The paint() method overrides the paint() method of the applet class and draws values to the input area.

The heart of this class is the event handler. As with other event handlers in the SpreadSheet applet, this one checks for events directly using methods of the Component class. Here, the keyDown event is checked to determine when a key is pressed.

The keyDown() method converts the value of the pressed key to an integer and then checks for three specific key types using their integer values. The integer value of the delete key is 8. When the Delete key is pressed, one character is deleted from the buffer[] array. The integer value of the Enter key is 10. When the Enter key is pressed, the current value of the buffer[] array is passed back to the cell. This happens behind the scenes after the empty selected() method of the InputField class is called. By default, the keyDown() method stores the value of any keypress other than Delete or Enter to the buffer[] array.


Listing 20.21. The InputField class.
class InputField {
    int         maxchars = 50;   //the maximum characters for the input field
    int         cursorPos = 0;   //index to the current cursor position
    Applet      app;   //instance of the applet object
    String      sval;    //container for string values
    char        buffer[];   //an array of characters up to maxchars in length
    int         nChars;    //the number of characters in the input area
    int         width;   //width of the input area
    int         height;   //height of the input area
    Color       bgColor;   //background color of the input area
    Color       fgColor;   //font color for text in the input area

    public InputField(String initValue, Applet app, int width, int height,
                      Color bgColor, Color fgColor) {
        this.width = width;
        this.height = height;
        this.bgColor = bgColor;
        this.fgColor = fgColor;
        this.app = app;
        buffer = new char[maxchars];
        nChars = 0;
        if (initValue != null) {
            initValue.getChars(0, initValue.length(), this.buffer, 0);
            nChars = initValue.length();
        }
        sval = initValue;
    }
    public void setText(String val) {
        int i;

        for (i=0; i < maxchars; i++) {
            buffer[i] = 0;
        }
        sval = new String(val);
        if (val == null) {
            sval = "";
            nChars = 0;
            buffer[0] = 0;
        } else {
            sval.getChars(0, sval.length(), buffer, 0);
            nChars = val.length();
            sval = new String(buffer);
        }
    }

    public void paint(Graphics g, int x, int y) {
        g.setColor(bgColor);
        g.fillRect(x, y, width, height);
        if (sval != null) {
            g.setColor(fgColor);
            g.drawString(sval, x, y + (height / 2) + 3);
        }
    }

    public void keyDown(int key) {
        if (nChars < maxchars) {
            switch (key) {
              case 8: // delete
                --nChars;
                if (nChars < 0) {
                    nChars = 0;
                }
                buffer[nChars] = 0;
                sval = new String(new String(buffer));
                break;
              case 10: // return
                selected();
                break;
              default:
                buffer[nChars++] = (char)key;
                sval = new String(new String(buffer));
                break;
            }
        }
        app.repaint();
    }
    public void selected() {
    }
}

The Complete Applet

Listing 21.22 shows the complete code for the SpreadSheet applet. Although you can go straight to the CD-ROM and access the complete source code, typing it in line by line forces you to study each line of code.


Listing 20.22. The SpreadSheet applet.
/*
 * @(#)SpreadSheet.java 1.17 95/03/09 Sami Shaio
 *
 * Copyright (c) 1994-1995 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Permission to use, copy, modify, and distribute this software
 * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and
 * without fee is hereby granted.
 * Please refer to the file http://java.sun.com/copy_trademarks.html
 * for further important copyright and trademark information and to
 * http://java.sun.com/licensing.html for further important licensing
 * information for the Java (tm) Technology.
 *
 * Modified for Peter Norton's Guide to Programming Java by William R. Stanek
 * 21 March 1996
 * @version 2.02
 *
 * These modifications include:
 *
 * Added parameter support for publisher defined title fonts, base fonts
 * for columns and rows, and font sizes.
 * Updated the title, column and row positions/sizes to make them dynamic and
 * thus support publisher defined font types and sizes.
 * Changed display characteristics.
 * Eliminates a display bug.
 * Adds comment descriptions to methods
 * Streamlined some class objects
 *
 * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF
 * THE SOFTWARE, EITHER EXPRESS OR IMPLIED, IncLUDING BUT NOT LIMITED
 * TO THE IMPLIED WARRANTIES OF MERchANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR
 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
 *
 * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE
 * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE
 * PERFORMAncE, SUch AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT
 * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE
 * SUPPORT MAchINES, OR WEAPONS SYSTEMS, IN WHIch THE FAILURE OF THE
 * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE
 * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES").  SUN
 * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR
 * HIGH RISK ACTIVITIES.
 */

import java.applet.Applet;
import java.awt.*;
import java.io.*;
import java.lang.*;
import java.net.*;

public class SpreadSheet extends Applet {
    //container to hold the parameter value for title text
    String              title;  
    //container to hold the parameter value for base font
    String              bfont;  
    //container to hold the parameter value for title font
    String              tfont;  
    //container for the inputfont object
    Font                inputFont;  
    //container for the title font object
    Font                titleFont;  
    //container for the cell color object
    Color              cellColor;  
    //container for the input area's color object
    Color              inputColor;  
    //container for the parameter value for font size
    int                 fontSize;  
    //container for the cell width object
    int                 cellWidth;  
    //container for the cell height object
    int                 cellHeight;  
    //container for the title height object
    int                 titleHeight;  
    //container for the row label width object
    int                 rowLabelWidth;  
    //container for the status object
    boolean             isStopped = false;  
    //container for the update object
    boolean             fullUpdate = true;  
    //container for the rows object
    int                 rows;  
    //container for the columns object
    int                 columns;  
    //container for the current key object
    int                 currentKey = -1;  
    //container for the selected row object
    int                 selectedRow = -1;  
    //container for the selected column object
    int                 selectedColumn = -1;  
    //container for the input area
    SpreadSheetInput    inputArea;  
    //container for the individual cells
    Cell                cells[][];  
    //container for the current cell
    Cell                current = null;  

    public synchronized void init() {
        String rs;
        String fs;

        cellColor = Color.white;
        inputColor = new Color(100, 100, 225);

        fs = getParameter("fontsize");
        if (fs == null) {
            fontSize = 10;
        } else {
            fontSize = Integer.parseInt(fs);
        }

    cellWidth = fontSize * 10;
    cellHeight = fontSize * 2;
    titleHeight = fontSize * 2;
    rowLabelWidth = fontSize * 3;

        bfont = getParameter("basefont");
        if (bfont == null) {
            bfont = "Courier";
        }
        inputFont = new Font(bfont, Font.PLAIN, fontSize);

        tfont = getParameter("titlefont");
        if (tfont == null) {
            tfont = "Courier";
        }
        titleFont = new Font(tfont, Font.BOLD, (fontSize + 2));

        title = getParameter("title");
        if (title == null) {
            title = "Spreadsheet";
        }
        rs = getParameter("rows");
        if (rs == null) {
            rows = 9;
        } else {
            rows = Integer.parseInt(rs);
        }
        rs = getParameter("columns");
        if (rs == null) {
            columns = 5;
        } else {
            columns = Integer.parseInt(rs);
        }
        cells = new Cell[rows][columns];
        char l[] = new char[1];
        for (int i=0; i < rows; i++) {
            for (int j=0; j < columns; j++) {

                cells[i][j] = new Cell(this,
                                       Color.lightGray,
                                       Color.black,
                                       cellColor,
                                       cellWidth - (fontSize/4),
                                       cellHeight - (fontSize/4));
                l[0] = (char)((int)'a' + j);
                rs = getParameter("" + new String(l) + (i+1));
                if (rs != null) {
                    cells[i][j].setUnparsedValue(rs);
                }
            }
        }

        Dimension d = size();
        inputArea = new SpreadSheetInput(null, this, d.width - 2,
               cellHeight - 2, inputColor, Color.white);
        resize(columns * cellWidth + rowLabelWidth,
               ((rows + 1) * cellHeight) + cellHeight + titleHeight);
    }

    public void setCurrentValue(float val) {
        if (selectedRow == -1 || selectedColumn == -1) {
            return;
        }
        cells[selectedRow][selectedColumn].setValue(val);
        repaint();
    }

    public void stop() {
        isStopped = true;
    }

    public void start() {
        isStopped = false;
    }

    public void destroy() {
        for (int i=0; i < rows; i++) {
            for (int j=0; j < columns; j++) {
                if (cells[i][j].type == Cell.URL) {
                    cells[i][j].updaterThread.stop();
                }
            }
        }
    }

    public void setCurrentValue(int type, String val) {
        if (selectedRow == -1 || selectedColumn == -1) {
            return;
        }
        cells[selectedRow][selectedColumn].setValue(type, val);
        repaint();
    }

    public void update(Graphics g) {
        if (! fullUpdate) {
            int cx, cy;

            g.setFont(titleFont);
            for (int i=0; i < rows; i++) {
                for (int j=0; j < columns; j++) {
                    if (cells[i][j].needRedisplay) {
                        cx = (j * cellWidth) + (fontSize*2) + rowLabelWidth;
                        cy = ((i+1) * cellHeight) + (fontSize*2) + titleHeight;
                        cells[i][j].paint(g, cx, cy);
                    }
                }
            }
        } else {
            paint(g);
            fullUpdate = false;
        }
    }

    public void recalculate() {
        int     i,j;

        //System.out.println("SpreadSheet.recalculate");
        for (i=0; i < rows; i++) {
            for (j=0; j < columns; j++) {
                if (cells[i][j] != null && cells[i][j].type == Cell.FORMULA) {
                    cells[i][j].setRawValue(evaluateFormula
           Â(cells[i][j].parseRoot));
                    cells[i][j].needRedisplay = true;
                }
            }
        }
        repaint();
    }

    public float evaluateFormula(Node n) {
        float   val = 0.0f;

        //System.out.println("evaluateFormula:");
        //n.print(3);
        if (n == null) {
            //System.out.println("Null node");
            return val;
        }
        switch (n.type) {
          case Node.OP:
            val = evaluateFormula(n.left);
            switch (n.op) {
              case '+':
                val += evaluateFormula(n.right);
                break;
              case '*':
                val *= evaluateFormula(n.right);
                break;
              case '-':
                val -= evaluateFormula(n.right);
                break;
              case '/':
                val /= evaluateFormula(n.right);
                break;
            }
            break;
          case Node.VALUE:
            //System.out.println("=>" + n.value);
            return n.value;
          case Node.CELL:
            if (n == null) {
                //System.out.println("NULL at 192");
            } else {
                if (cells[n.row][n.column] == null) {
                    //System.out.println("NULL at 193");
                } else {
                    //System.out.println("=>" + cells[n.row][n.column].value);
                    return cells[n.row][n.column].value;
                }
            }
        }

        //System.out.println("=>" + val);
        return val;
    }

    public synchronized void paint(Graphics g) {

        int i, j;
        int cx, cy;
        char l[] = new char[1];

        Dimension d = size();

        //draws the title on the frame
        g.setFont(titleFont);
        i = g.getFontMetrics().stringWidth(title);
        g.drawString((title == null) ? "Spreadsheet" : title,
                     (d.width - i + (fontSize*3/2)) / 2, (fontSize*3/2));

        //draws the input area on the frame
        g.setColor(inputColor);
        g.fillRect(0, cellHeight, d.width, cellHeight);

        //draws the lines for rows using 3d effect
        //adds row numbering in blue
        g.setFont(titleFont);
        for (i=0; i < rows+1; i++) {
            cy = (i+2) * cellHeight;
            g.setColor(getBackground());
            g.draw3DRect(0, cy, d.width, 2, true);
            if (i < rows) {
                g.setColor(Color.blue);
                g.drawString("" + (i+1), fontSize, cy + (fontSize *3/2));
            }
        }

        //draws the lines for columns using 3d effect
        //adds column alphas in red
        g.setColor(Color.red);
        for (i=0; i < columns; i++) {
            cx = i * cellWidth;
            g.setColor(getBackground());
            g.draw3DRect(cx + rowLabelWidth,
                          2 * cellHeight, 1, d.height, true);
            if (i < columns) {
                g.setColor(Color.red);
                l[0] = (char)((int)'A' + i);
                g.drawString(new String(l),
                             cx + rowLabelWidth + (cellWidth / 2),
                             d.height - 3);
            }
        }

        //paints the cell data
        for (i=0; i < rows; i++) {
            for (j=0; j < columns; j++) {
                cx = (j * cellWidth) + 2 + rowLabelWidth;
                cy = ((i+1) * cellHeight) + 2 + titleHeight;
                if (cells[i][j] != null) {
                    cells[i][j].paint(g, cx, cy);
                }
            }
        }

        g.setColor(getBackground());
       //draws a 3d line for the input area and to the left side of the frame
        g.draw3DRect(0, titleHeight, d.width, d.height - titleHeight, false);
        //paints the data of the current cell to the input area
        inputArea.paint(g, 1, titleHeight + 1);
    }

    public boolean mouseDown(Event evt, int x, int y) {
        Cell cell;
//ensures the y coordinate of the mouse pointer
        //is not in the title/input area
        if (y < (titleHeight + cellHeight)) {
            selectedRow = -1;
            if (y <= titleHeight && current != null) {
                current.deselect();
                current = null;
            }
            return true;
        }
//ensures the x coordinate of the mouse pointer
        //is not in the row label area
        if (x < rowLabelWidth) {
            selectedRow = -1;
            if (current != null) {
                current.deselect();
                current = null;
            }
            return true;
        }

        //determines the row of the selected cell
        selectedRow = ((y - cellHeight - titleHeight) / cellHeight);

        //determines the column of the selected cell
        selectedColumn = (x - rowLabelWidth) / cellWidth;

        //ensures the row and column coordinate is valid
        if (selectedRow >= rows ||
            selectedColumn >= columns) {
            selectedRow = -1;
            if (current != null) {
                current.deselect();
                current = null;
            }

        //if the row-column coordinate is valid place the selected cells value
        //in the input area
        } else {
            cell = cells[selectedRow][selectedColumn];
            inputArea.setText(new String(cell.getPrintString()));
            if (current != null) {
                current.deselect();
            }
            current = cell;
            current.select();
            requestFocus();
            fullUpdate = true;
            repaint();
        }
        return true;
    }

    //determine if a key is pressed
    public boolean keyDown(Event evt, int key) {
        fullUpdate=true;
        inputArea.keyDown(key);
        return true;
    }
}

class CellUpdater extends Thread {
    Cell        target;
    InputStream dataStream = null;
    StreamTokenizer tokenStream;

    public CellUpdater(Cell c) {
        super("cell updater");
        target = c;
    }

    public void run() {
        try {
            dataStream = new URL(target.app.getDocumentBase(),
                                 target.getValueString()).openStream();
            tokenStream = new StreamTokenizer(dataStream);
            tokenStream.eolIsSignificant(false);

            while (true) {
                switch (tokenStream.nextToken()) {
                case tokenStream.TT_EOF:
                    dataStream.close();
                    return;
                default:
                    break;
                case tokenStream.TT_NUMBER:
                    target.setTransientValue((float)tokenStream.nval);
                    if (! target.app.isStopped && ! target.paused) {
                        target.app.repaint();
                    }
                    break;
                }
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        } catch (IOException e) {
            return;
        }
    }
}

class Cell {
    public static final int VALUE = 0;   //initializes the VALUE container
    public static final int LABEL = 1;   //initializes the LABEL container
    public static final int URL   = 2;   //initializes the URL container
    public static final int FORMULA = 3;   //initializes the FORMULA container
    Node        parseRoot;   //sets the node for the cell
    boolean     needRedisplay;   //used to determine the need for redisplay
    boolean selected = false;    //used to determine whether cell is selected
    boolean transientValue = false;   //tracks need to set transient value
    public int  type = Cell.VALUE;    //sets the cell type
    String      valueString = "";    //initializes the value string
    String      printString = "v";   //initiliazes the print string
    float       value;   //container for a float called value
    Color       bgColor;   //container for the background color
    Color       fgColor;   //container for the foreground color
    Color       highlightColor;   //container for cell color when selected
    int         width;   //container for the width of the cell
    int         height;   //container for the height of the cell
    SpreadSheet app;    //container for the SpreadSheet object
    CellUpdater updaterThread;   //container for the CellUpdater object
    boolean     paused = false;    //used to determine if paused

    public Cell(SpreadSheet app,
                Color bgColor,
                Color fgColor,
                Color highlightColor,
                int width,
                int height) {
        this.app = app;
        this.bgColor = bgColor;
        this.fgColor = fgColor;
        this.highlightColor = highlightColor;
        this.width = width;
        this.height = height;
        needRedisplay = true;
    }

    public void setRawValue(float f) {
        valueString = Float.toString(f);
        value = f;
    }
    public void setValue(float f) {
        setRawValue(f);
        printString = "v" + valueString;
        type = Cell.VALUE;
        paused = false;
        app.recalculate();
        needRedisplay = true;
    }

    public void setTransientValue(float f) {
        transientValue = true;
        value = f;
        needRedisplay = true;
        app.recalculate();
    }

    public void setUnparsedValue(String s) {
        switch (s.charAt(0)) {
          case 'v':
            setValue(Cell.VALUE, s.substring(1));
            break;
          case 'f':
            setValue(Cell.FORMULA, s.substring(1));
            break;
          case 'l':
            setValue(Cell.LABEL, s.substring(1));
            break;
          case 'u':
            setValue(Cell.URL, s.substring(1));
            break;
        }
 }

    /**
     * Parse a spreadsheet formula. The syntax is defined as:
     *
     * formula -> value
     * formula -> value op value
     * value -> '(' formula ')'
     * value -> cell
     * value -> <number>
     * op -> '+' | '*' | '/' | '-'
     * cell -> <letter><number>
     */
    public String parseFormula(String formula, Node node) {
        String subformula;
        String restFormula;
        float value;
        int length = formula.length();
        Node left;
        Node right;
        char op;

        if (formula == null) {
            return null;
        }
        subformula = parseValue(formula, node);
        //System.out.println("subformula = " + subformula);
        if (subformula == null || subformula.length() == 0) {
            //System.out.println("Parse succeeded");
            return null;
        }
        if (subformula == formula) {
            //System.out.println("Parse failed");
            return formula;
        }

        // parse an operator and then another value
        switch (op = subformula.charAt(0)) {
          case 0:
            //System.out.println("Parse succeeded");
            return null;
          case ')':
            //System.out.println("Returning subformula=" + subformula);
            return subformula;
          case '+':
          case '*':
          case '-':
          case '/':
            restFormula = subformula.substring(1);
            subformula = parseValue(restFormula, right=new Node());
            //System.out.println("subformula(2) = " + subformula);
            if (subformula != restFormula) {
                //System.out.println("Parse succeeded");
                left = new Node(node);
                node.left = left;
                node.right = right;
                node.op = op;
                node.type = Node.OP;
                //node.print(3);
                return subformula;
            } else {
                //System.out.println("Parse failed");
                return formula;
            }
          default:
            //System.out.println("Parse failed (bad operator): " + subformula);
            return formula;
        }
    }

    public String parseValue(String formula, Node node) {
        char    c = formula.charAt(0);
        String  subformula;
        String  restFormula;
        float   value;
        int     row;
        int     column;

        //System.out.println("parseValue: " + formula);
        restFormula = formula;
        if (c == '(') {
            //System.out.println("parseValue(" + formula + ")");
            restFormula = formula.substring(1);
            subformula = parseFormula(restFormula, node);
            //System.out.println("rest=(" + subformula + ")");
            if (subformula == null ||
                subformula.length() == restFormula.length()) {
                //System.out.println("Failed");
                return formula;
            } else if (! (subformula.charAt(0) == ')')) {
                //System.out.println("Failed (missing parentheses)");
                return formula;
            }
            restFormula = subformula;
        } else if (c >= '0' && c <= '9') {
            int i;

            //System.out.println("formula=" + formula);
            try {
                value = Float.valueOf(formula).floatValue();
            } catch (NumberFormatException e) {
                //System.out.println("Failed (number format error)");
                return formula;
            }
            for (i=0; i < formula.length(); i++) {
                c = formula.charAt(i);
                if ((c < '0' || c > '9') && c != '.') {
                    break;
                }
            }
            node.type = Node.VALUE;
            node.value = value;
            //node.print(3);
            restFormula = formula.substring(i);
            //System.out.println("value= " + value + " i=" + i +
                //                     " rest = " + restFormula);
            return restFormula;
        } else if (c >= 'A' && c <= 'Z') {
            int i;

            column = c - 'A';
            restFormula = formula.substring(1);
            row = Float.valueOf(restFormula).intValue();
            //System.out.println("row = " + row + " column = " + column);
            for (i=0; i < restFormula.length(); i++) {
                c = restFormula.charAt(i);
                if (c < '0' || c > '9') {
                    break;
                }
            }
            node.row = row - 1;
            node.column = column;
            node.type = Node.CELL;
            //node.print(3);
            if (i == restFormula.length()) {
                restFormula = null;
            } else {
                restFormula = restFormula.substring(i);
                if (restFormula.charAt(0) == 0) {
                    return null;
                }
            }
        }

        return restFormula;
    }


    public void setValue(int type, String s) {
        paused = false;
        if (this.type == Cell.URL) {
            updaterThread.stop();
            updaterThread = null;
        }

        valueString = new String(s);
        this.type = type;
        needRedisplay = true;
        switch (type) {
          case Cell.VALUE:
            setValue(Float.valueOf(s).floatValue());
            break;
          case Cell.LABEL:
            printString = "l" + valueString;
            break;
          case Cell.URL:
            printString = "u" + valueString;
            updaterThread = new CellUpdater(this);
            updaterThread.start();
            break;
          case Cell.FORMULA:
            parseFormula(valueString, parseRoot = new Node());
            printString = "f" + valueString;
            break;
        }
        app.recalculate();
    }

    public String getValueString() {
        return valueString;
    }

    public String getPrintString() {
        return printString;
    }

    public void select() {
        selected = true;
        paused = true;
    }
    public void deselect() {
        selected = false;
        paused = false;
        needRedisplay = true;
    }
    public void paint(Graphics g, int x, int y) {
        if (selected) {
            g.setColor(highlightColor);
        } else {
            g.setColor(bgColor);
        }
        g.fillRect(x, y, width - 1, height);
        if (valueString != null) {
            switch (type) {
              case Cell.VALUE:
              case Cell.LABEL:
                g.setColor(fgColor);
                break;
              case Cell.FORMULA:
                g.setColor(Color.red);
                break;
              case Cell.URL:
                g.setColor(Color.blue);
                break;
            }
            if (transientValue){
                g.drawString("" + value, x, y + (height / 2) + 5);
            } else {
                if (valueString.length() > 14) {
                    g.drawString(valueString.substring(0, 14),
                                 x, y + (height / 2) + 5);
                } else {
                    g.drawString(valueString, x, y + (height / 2) + 5);
                }
            }
        }
        needRedisplay = false;
    }
}

class Node {
    //set for node components that are operands
    public static final int OP = 0;
    //set for node components that are values
    public static final int VALUE = 1;
    //set for node components that are cell references
    public static final int CELL = 2;

    int         type;
    Node        left;   //the lefthand value for the node
    Node        right;   //the righthand value for the node
    int         row;   //the row coordinate of the selected cell
    int         column;   //the column coordinate of the selected cell
    float       value;   //the current value
    char        op;   //the operand +, -, /, *

    public Node() {
        left = null;
        right = null;
        value = 0;
        row = -1;
        column = -1;
        op = 0;
        type = Node.VALUE;
    }
    public Node(Node n) {
        left = n.left;
        right = n.right;
        value = n.value;
        row = n.row;
        column = n.column;
        op = n.op;
        type = n.type;
    }
    public void indent(int ind) {
        for (int i = 0; i < ind; i++) {
            System.out.print(" ");
        }
    }
    public void print(int indentLevel) {
        char l[] = new char[1];
        indent(indentLevel);
        System.out.println("NODE type=" + type);
        indent(indentLevel);
        switch (type) {
          case Node.VALUE:
            System.out.println(" value=" + value);
            break;
          case Node.CELL:
            l[0] = (char)((int)'A' + column);
            System.out.println(" cell=" + new String(l) + (row+1));
            break;
          case Node.OP:
            System.out.println(" op=" + op);
            left.print(indentLevel + 3);
            right.print(indentLevel + 3);
            break;
        }
    }
}

class InputField {
    int         maxchars = 50;   //the maximum characters for the input field
    int         cursorPos = 0;   //index to the current cursor position
    Applet      app;   //instance of the applet object
    String      sval;    //container for string values
    char        buffer[];   //an array of characters up to maxchars in length
    int         nChars;    //the number of characters in the input area
    int         width;   //width of the input area
    int         height;   //height of the input area
    Color       bgColor;   //background color of the input area
    Color       fgColor;   //font color for text in the input area

    public InputField(String initValue, Applet app, int width, int height,
                      Color bgColor, Color fgColor) {
        this.width = width;
        this.height = height;
        this.bgColor = bgColor;
        this.fgColor = fgColor;
        this.app = app;
        buffer = new char[maxchars];
        nChars = 0;
        if (initValue != null) {
            initValue.getChars(0, initValue.length(), this.buffer, 0);
            nChars = initValue.length();
        }
        sval = initValue;
    }

    public void setText(String val) {
        int i;

        for (i=0; i < maxchars; i++) {
            buffer[i] = 0;
        }
        sval = new String(val);
        if (val == null) {
            sval = "";
            nChars = 0;
            buffer[0] = 0;
        } else {
            sval.getChars(0, sval.length(), buffer, 0);
            nChars = val.length();
            sval = new String(buffer);
        }
    }

    public void paint(Graphics g, int x, int y) {
        g.setColor(bgColor);
        g.fillRect(x, y, width, height);
        if (sval != null) {
            g.setColor(fgColor);
            g.drawString(sval, x, y + (height / 2) + 3);
        }
    }
    public void keyDown(int key) {
        if (nChars < maxchars) {
            switch (key) {
              case 8: // delete
                --nChars;
                if (nChars < 0) {
                    nChars = 0;
                }
                buffer[nChars] = 0;
                sval = new String(new String(buffer));
                break;
              case 10: // return
                selected();
                break;
              default:
                buffer[nChars++] = (char)key;
                sval = new String(new String(buffer));
                break;
            }
        }
        app.repaint();
    }
    public void selected() {
    }
}

class SpreadSheetInput extends InputField {
    public SpreadSheetInput(String initValue,
                            SpreadSheet app,
                            int width,
                            int height,
                            Color bgColor,
                            Color fgColor) {
        super(initValue, app, width, height, bgColor, fgColor);
    }

    public void selected() {
        float f;

        switch (sval.charAt(0)) {
          case 'v':
            try {
                f = Float.valueOf(sval.substring(1)).floatValue();
                ((SpreadSheet)app).setCurrentValue(f);
            } catch (NumberFormatException e) {
                System.out.println("Not a float...");
            }
            break;
          case 'l':
            ((SpreadSheet)app).setCurrentValue(Cell.LABEL, sval.substring(1));
            break;
          case 'u':
            ((SpreadSheet)app).setCurrentValue(Cell.URL, sval.substring(1));
            break;
          case 'f':
           ((SpreadSheet)app).setCurrentValue(Cell.FORMULA, sval.substring(1));
            break;
        }
    }
}

Putting the SpreadSheet Applet to Use

To use the SpreadSheet applet, you need to create an HTML document that calls the applet. The document can be as simple or as complex as you want it to be. The code for a simple HTML document that causes the SpreadSheet applet to use default values is shown in Listing 20.23. This document is shown in Figure 20.3.

Figure 20.3 : A document using the SpreadSheet applet's defaultsettings.


Listing 20.23. A simple HTML document using the SpreadSheet applet.
<HTML>
<HEAD>
<TITLE>Default SpreadSheet</TITLE>
</HEAD>
<BODY>
<APPLET CODE="SpreadSheet.class" WIDTH=400 HEIGHT=250>
</APPLET>
</BODY>
</HTML>

Although your HTML document does not have to set parameter values for the spreadsheet, the best way to see how the applet behaves is to experiment with parameter values. Listing 20.24 shows how an advanced spreadsheet was built using the SpreadSheet applet. This document is shown earlier in this chapter as Figure 20.2.


Listing 20.24. A more advanced example using the SpreadSheet applet.
<HTML>
<HEAD>
<TITLE>SpreadSheet</TITLE>
</HEAD>
<BODY>
<HR>
<APPLET CODE="SpreadSheet.class" WIDTH=750 HEIGHT=400>
<PARAM NAME=title value="Java Powered Spreadsheet">
<PARAM NAME=columns value="5">
<PARAM NAME=rows value="7">
<PARAM NAME=fontsize value="14">
<PARAM NAME=basefont value="TimesRoman">
<PARAM NAME=titlefont value="Helvetica">
<PARAM NAME=a1 value="lWholesale">
<PARAM NAME=a2 value="v20195">
<PARAM NAME=a3 value="v7280">
<PARAM NAME=a4 value="v6127">
<PARAM NAME=a5 value="v9803">
<PARAM NAME=a6 value="v4150">
<PARAM NAME=a7 value="fA2+(A3+(A4+(A5+A6)))">
<PARAM NAME=b1 value="lShipping">
<PARAM NAME=b2 value="v1500">
<PARAM NAME=b3 value="v350">
<PARAM NAME=b4 value="v729">
<PARAM NAME=b5 value="v830">
<PARAM NAME=b6 value="v125">
<PARAM NAME=b7 value="fB2+(B3+(B4+(B5+B6)))">
<PARAM NAME=c1 value="lPackaging">
<PARAM NAME=c2 value="v120">
<PARAM NAME=c3 value="v35">
<PARAM NAME=c4 value="v48">
<PARAM NAME=c5 value="v52">
<PARAM NAME=c6 value="v12">
<PARAM NAME=c7 value="fC2+(C3+(C4+(C5+C6)))">
<PARAM NAME=d1 value="lTotal Cost">
<PARAM NAME=d2 value="fA2+(B2+C2)">
<PARAM NAME=d3 value="fA3+(B3+C3)">
<PARAM NAME=d4 value="fA4+(B4+C4)">
<PARAM NAME=d5 value="fA5+(B5+C5)">
<PARAM NAME=d6 value="fA6+(B6+C6)">
<PARAM NAME=d7 value="fD2+(D3+(D4+(D5+D6)))">
<PARAM NAME=e1 value="lCost/Expense">
<PARAM NAME=e2 value="fA2/(B2+C2)">
<PARAM NAME=e3 value="fA3/(B3+C3)">
<PARAM NAME=e4 value="fA4/(B4+C4)">
<PARAM NAME=e5 value="fA5/(B5+C5)">
<PARAM NAME=e6 value="fA6/(B6+C6)">
</APPLET>
<HR>
<A HREF="SpreadSheet.java">The source.</A>
</BODY>
</HTML>

Summary

After completing this chapter, you should have a good understanding of how to develop advanced applets using the Java programming language. Although you may be able to develop and implement some applets in a single day, more advanced projects are developed and implemented over a period of days, weeks, or months. Most advanced projects include six phases: requirements, specification/analysis, planning, design, implementation, and testing.

The SpreadSheet applet developed in this chapter has many characteristics of advanced applets. Now that you have examined it, you should have a better understanding of using text and graphics, manipulating text strings, using mathematical formulas, and processing user input.