Lab 9: Drawer, Part II

Objectives

In this lab we'll add two new tools to our drawing program: a tool for drawing circles in the canvas, and a tool for deleting shapes from the canvas. You can run the command ``drawing 2'' to see how your program should work after this lab.

To begin, type the following to set up the directory for Lab 9.

% getcs 160 9
% cp ~/CS160/Lab7/*.java ~/CS160/Lab9
In Forte, close all currently open files in the editor (by repeatedly right-clicking in the Editor window and selecting Close). Then, modify the package lines of the files from the Lab9 package to reflect the new directory.

Part A. Iterative development

This lab illustrates the concept of iterative development, which means that we'll break the task into smaller pieces. For each step, you'll conclude with a complete, working program, which you should test completely before going onto the next step.

In this lab, we'll split the overall task into four steps. (Deciding what the individual, bite-size steps are takes some experience. In this case, the lab does that part of the job for you.)

  1. We'll empower a Drawing object to represent any combination of shapes (not just rectangles).

  2. We'll empower a Canvas object to modify its behavior for mouse events, using the new abstract Tool class. (Neither of these first two steps actually adds any new capabilities to the running program - they're just setting us up for the next two steps. But they're still major changes.)

  3. We'll add the Circle and CircleTool classes so that the user can add circles to the drawing area.

  4. We'll add the DeleteTool class to allow the user to delete shapes from the drawing area.

After each step, it's essential that you completely test your program so far. Complete testing requires that you run your program and try a wide selection of different scenarios to verify that the program is still working.

Iterative development is a good habit to develop, since it makes the process of finding errors simpler. This is because, after each small step, you've only made a few changes. When you find errors during the testing process, you'll know that the fix must lie within the small set of changes.

Part B. Structure

When we are done, our program will have the following inheritance hierarchy. Two of the classes, Shape and Tool, are abstract classes and are represented in italics.

Step 1. The Shape

classes

The first step is to alter Drawing so that it can hold any combination of shapes. To accomplish this, we'll define a Shape class. It will be abstract, since we'll never actually create a shape per se - the class exists only so that we can refer to rectangles and circles as abstract shapes within the Drawing class. (Drawing will never refer directly to Rectangle. Instead, it will just treat every object in its ArrayList as a Shape.)

The Shape class will define the single instance method draw().

abstract void draw(Graphics g)
Draws the shape using g.

Define the abstract Shape class, and modify Rectangle so that it extends Shape. Finally, modify Drawing so that it can hold any combination of Shapes. This should require only that you replace each reference to the Rectangle class in Drawing.java with Shape instead.

Step 2. The Tool

classes

The purpose of a Tool object is to define what Canvas will do when the user drags the mouse. The abstract Tool class defines two abstract methods.

abstract void mouseDragging(Canvas c, int x0, int y0, int x1, int y1)
Defines what to do when the user is in the process of dragging from (x0,y0) in the canvas and has currently reached (x1,y1).

abstract void mouseDragged(Canvas c, int x0, int y0, int x1, int y1)
Defines what to do when the user has dragged the mouse from (x0,y0) to (x1,y1) in the canvas.

The first subclass of Tool, RectangleTool, will simply accomplish what the mouseDragging() and mouseDragged() methods of Canvas from Lab 7.

The Canvas class will have a Tool instance variable (whose initial value is an instance of RectangleTool) to track the currently selected tool. Its mouseDragging() and mouseDragged() methods will simply call the corresponding method of the current tool.

Before continuing with this lab, you should complete and test all of the above. When you run your program, it should work exactly as in Lab 7. So far, it doesn't look like much progress, but we've set things up so that adding the new features will be easier.

Step 3. The Circle and CircleTool classes

Now we'll add the capability to draw circles. First, define a new concrete class Circle that extends Shape. It will have the following constructor.

Circle(Color color, int x, int y, int r)
Creates a circle of color color and radius r, centered at (x,y).

Of course, it will have to define draw(), since it extends Shape.

Also, define a concrete CircleTool class, extending Tool. Its definition will be quite similar to the RectangleTool class.

Finally, modify Canvas so that it now has a Tool menu, with both Rectangle and Circle options. Selecting the menu item should simply change the Canvas's Tool instance variable, so that subsequent mouse events get forwarded to this other Tool object instead.

Step 4. The DeleteTool

class

When the deletion tool is selected, the shape should disappear from the canvas when the user clicks a shape. To accomplish this, perform the following tasks.

  1. Add the following method to the Shape class and to its subclasses.

    abstract boolean contains(int x, int y)
    Returns true if the point (x,y) lies inside the shape.

  2. Add the following two methods to the Drawing class.

    Shape find(int x, int y)
    Returns a shape containing the point (x, y). If there are no shapes containing the point, it returns null. In the case of a tie, it should return the topmost shape (i.e., the last shape added).

    void removeShape(Shape to_remove)
    Removes the shape to_remove from the drawing.

  3. Define DeleteTool, a new subclass of Tool. This tool doesn't do anything while the user is dragging. But, after the user has finished dragging, it should delete the shape in which the user has clicked. (To see whether the user has clicked in the shape, the program should see in which shape the user pressed the mouse and in which shape the user released the mouse. If both events occurred in the same shape, then the user has clicked in the shape.)

  4. Add a Delete menu item to the Tool menu.

Part C. Observations

This lab illustrates how you can polymorphism (the ability of an object to masquerade as an instance of its superclass) and overriding (the ability of an object to redefine instance methods) to simplify a program and avoid explicitly testing for various cases.

Before we learned about inheritance at all, the way we would accomplish having different tools for the program would be to keep an integer variable, whose value would be 0 for creating rectangles, 1 for creating circles, and 2 for deleting shapes. Then, in the mouseDragging() and mouseDragged() method of Canvas, we would write something like the following.

if(tool == 0) {
	// do what the rectangle tool should do
} else if(tool == 1) {
	// do what the circle tool should do
} else if(tool == 2) {
	// do what the delete tool should do
}
I'll call this the else if technique. It gets to be a pain, but that's what we would have to do in order to handle the various cases.

Instead, we defined the abstract Tool class, and had an instance variable within Canvas to track the a current Tool instance. In mouseDragging() and mouseDragged(), we simply forward the call onto the current Tool instance. Since each Tool subclass overrides these methods, the behavior is different based on what the current tool is.

We played a similar trick with shapes. Without inheritance, we would have to track what sort of shape each thing in the Drawing is. Instead, we simply defined a Shape class, and had each subclass define its own draw() and find() methods.

This trick has two major advantages. First, it makes for a cleaner structure, with distinct responsibilities split between files. This is itself a major virtue: With larger programs, ways to split what would otherwise be a huge class into smaller pieces becomes more desirable.

The second advantage it makes the program more easily extendible. Suppose, for example, that we wanted to add the capability to draw line segments to the program. We would of course define Line and LineTool classes. We wouldn't have to change Drawing at all, and the only modification to Canvas would be for putting the new menu item in. If we used the else if technique, the changes to Drawing and Canvas would be much more extensive.

This is a good heuristic to keep in mind with object-oriented design: If you see any place in the program where you have several cases, there's a good chance that you can avoid it using polymorphism and overriding, just as we have done in this lab. A strong indication that this is worth considering is when you are tempted to include a long string of else if's. Such strings of else if's are confusing and usually avoidable with a better object-oriented design.