PS-3, due Oct 21

In this assignment, you will create an object-oriented graphical editor applet. which will be both a lot of fun and learning. You will use inheritance in an interesting way to accomplish this editor.

Important organizational notes

For this assignment, you are permitted to work with one other student currently in the class. You do not have to work with someone else, but you have the option of doing so. If you choose to work with a partner, you will both get the same grade on the assignment.

There will be no penalty, in terms of points, for working together on this assignment. Please make sure that both of you submit the code electronically. When you submit on Blackboard, be sure to state the name of your partner in the comments section.

You should weigh whether you will get more out of this assignment working alone or with someone else. The choice is up to you.

If you choose to work with someone else, pick your partner carefully. Make sure that there are times that you are both available to work together. If you frequent the lab and you notice someone else who often is there when you are, that person might be a good choice as your partner. For this assignment there are a large number of files that need to be submitted. As always, keep all the java files (including the ones we supplied to you) in a folder, then create a zip file of that folder, and submit this zip file via Blackboard.

Graphical editor

Your job is to write the graphical editor that I have demonstrated in lecture. You can run it yourself:

Functional specification

The graphical editor allows you to create and edit three kinds of graphical objects—rectangles, ellipses, and line segments—in a drawing. The objects are linearly ordered, from front to back, so that if two objects overlap, the one in front is what you see. Each object can appear in either red, green, or blue. The editing operations allow you to change an object's color, to move an object by dragging it, to delete an object, to move an object to the front of the linear order, to move an object to the back of the linear order, and to exchange the locations of two objects.

You interact with the graphical editor by means of a simple, button-based GUI. Below the command buttons is a white canvas upon which all graphical objects appear. Initially the drawing is empty. The graphical editor maintains, at all times, the notion of a default color: the color in which added objects will be drawn. The default color appears in the GUI in a color indicator box, and initially the default color is red.

When a command button is pressed, that specifies how the editor is to react to click, press, and drag events in the canvas. The editor will continue reacting in that way until the next time that a command button is pressed. Here is how each of the buttons is to work:

Rectangle
After clicking the Rectangle button, you may drag out a rectangle on the canvas, from corner to corner. The rectangle continually appears on the canvas, and when the mouse is released, the rectangle is added to the drawing. The rectangle is drawn in the current default color, even as it's being dragged out.

Ellipse
After clicking the Ellipse button, you may drag out an ellipse on the canvas, from corner to corner of the bounding box. The ellipse continually appears on the canvas, and when the mouse is released, the ellipse is added to the drawing. The ellipse is drawn in the current default color, even as it's being dragged out.

Note: Although we call the shape an ellipse, you will need to call the fillOval method to draw ellipses. (Geometrically, the shape really is an ellipse, but the AWT calls the method fillOval, not fillEllipse.)

Line
After clicking the Line button, you may drag out a line segment on the canvas, from endpoint to endpoint. The line segment continually appears on the canvas, and when the mouse is released, the line segment is added to the drawing. The line segment is drawn in the current default color, even as it's being dragged out.

Move
After clicking the Move button, you may drag any object on the canvas. The frontmost object that is under the mouse position at the time that dragging starts (i.e., the button is pressed while the mouse is in the canvas) is the one that is moved. It is not an error for no object to be under the mouse position when dragging starts, but no object is moved in this case.

Delete
After clicking the Delete button, any time there is a click in the canvas, the frontmost object that is under the mouse position at the time of the click is deleted from the drawing. It is not an error for no object to be under the mouse position when a click occurs, but no object is deleted in this case.

Front/Back
After clicking the Front or Back button, any time there is a click in the canvas, the frontmost object that is under the mouse position at the time of the click is moved to either the front or back of the linear ordering of objects in the drawing. It is not an error for no object to be under the mouse position when a click occurs, but no object is moved to the front or back in this case.

Exchange
After clicking the Exchange button, any time two objects are clicked, they exchange their positions on the canvas. That is, if object X is the frontmost object that is under the mouse position at the time of a click, and then object Y is the frontmost object that is under the mouse position at the time of a second click, this command moves X to have the same center that Y had, and it moves Y to have the same center that X had. It is not an error for no object to be under the mouse position when a click occurs. This command works on objects in distinct pairs, so that if you click on objects X, Y, and Z, then X and Y exchange centers, and Z remains unchanged until such time as another object is clicked.

Red/Green/Blue
After clicking the Red, Green, or Blue button, two things happen. First, the default color (the color in which new objects are added) changes to the appropriate color, and the color indicator box changes color. Second, any time there is a click in the canvas, the frontmost object that is under the mouse position at the time of the click has its color changed appropriately. It is not an error for no object to be under the mouse position when a click occurs, but no object has its color changed in this case.

Design and implementation

This would certainly be a daunting project for you to write on your own, especially since we have covered many but not all details about GUIs in lecture. So I am supplying you with several Java files that will help you get started. You will have to fill in missing code in many of them, and there will be some Java files that you will have to write from scratch. Most of the Java classes that you will write are not very long, although one, for the Drawing class, will be longer than the others.

You can grab all the provided files in the zip file provided.zip. In alphabetical order, the provided files are:

The largest file by far is Editor.java. It contains the GUI. Each command button is a JButton object and has a corresponding object from an inner class that acts as a listener. For example, the JButton referenced by rectButton has a RectButtonListener object whose actionPerformed method is called every time the Rectangle button is clicked. Each command button has its own inner class to act as a listener, and each inner class's actionPerformed method is called when the appropriate command is button is clicked.

You'll notice that each actionPerformed method has the comment

// YOU FILL THIS IN. As you will see (I hope), what you have to fill in there is only one or a few lines of Java code. Each actionPerformed method ends with a call to repaint, which will cause the entire GUI and the entire canvas to be redisplayed.

You can leave the rest of Editor.java alone. Although it's a large file, you don't have to do much to it. But you'll need to understand what it does. We'll get to that later.

The Shape hierarchy

I have provided an abstract class Shape in Shape.java. This class defines a private instance variable color for the shape's color, and it has abstract methods drawShape, containsPoint, move, and getCenter. I have also included the definition of the methods setColor, which stores a color given as a parameter into the instance variable; draw, which draws the Shape, calling the abstract method drawShape; and setCenter, which moves an object so that it has a specific location (given by a Point) as its center.

You are required to create three subclasses of Shape: Rect, Ellipse, and Segment. You may use the ideas from the lecture that covered abstract classes, but you are not required to. (For example, you don't have to introduce an intermediate abstract class like PointRelativeShape. You may do so if you like, but you don't have to.) You should have no need to make any changes to the Shape class (except possibly to implement extra-credit features).

Obviously, the instance variables of these subclasses will have to include geometric information. And your subclass definitions will have to include definitions of drawShape, containsPoint, move, and getCenter. I have provided files Ellipse.java, and Segment.java for you to start from. The Ellipse.java and Segment.java files contain some private static helper methods for determining whether a given Point is within an Ellipse or within a given tolerance of a Segment:

The Command hierarchy

What is likely to seem strange at first is that you also must create and use classes for the commands. In Command.java, I have provided a superclass called Command. It has three methods—executeClick, executePress, and executeDrag—each with a default method body that is empty. Each of these methods takes two parameters, a reference to a Point and a reference to a Drawing. (We'll get to the Drawing class later.) You will create subclasses of Command for the various commands. You should not need to alter the Command class in any way.

If you go back to Editor.java and look at the instance variables of the Editor class, you'll see cmd, which is a reference to a Command object, and dwg, which is a reference to a Drawing object. The init method sets cmd to reference a new Command object, and it sets dwg to reference a new Drawing object. Let's focus on Command.

Near the bottom of Editor.java is an inner class, CanvasPanel, which defines the canvas upon which the objects are drawn. It acts as a listener for the mouse being clicked, pressed, or dragged. For example, take a look at the mouseClicked method. It is simply

public void mouseClicked(MouseEvent event) { cmd.executeClick(event.getPoint(), dwg); repaint(); } So, when the mouse is clicked on the canvas, we will call the executeClick method of whatever object cmd references at that moment. Initially, cmd references a Command object, and its executeClick method does nothing. Big deal.

Suppose, however, that we have some subclass of Command—oh, let's say DeleteCmd—and that cmd references a DeleteCmd object when a mouse click occurs in the canvas. Then that DeleteCmd object's executeClick method is called. It's given a Point that says where the mouse click occured, and it's given a reference to the Drawing object that represents the drawing. In other words, this executeClick method has all the information that it needs to find the frontmost object under the mouse position and, if there is such an object, to delete it from the drawing.

Gosh, how do we get this hypothetical DeleteCmd object to be referenced by cmd? That's actually mighty easy. First, let's think about under what circumstances we would even want cmd to reference a DeleteCmd object. That would be when the user has clicked the Delete button in the GUI. When the Delete button is clicked, the actionPerformed method of a DeleteButtonListener is called. So, other than calling repaint, the only thing that this actionPerformed method has to do is make cmd reference a DeleteCmd object. Where do you get this DeleteCmd object from? Just make one by using the new operator.

(Note: This idea of using an object to encapsulate the information about how an action should be performed is common enough that it has a name. It is called the "command pattern." Different objects contain different implementations, or "commands," for carrying out an operation. Instead of having a big "if ladder" (an if-else if-else if-…-else construct) to decide which case we are in, we simply assign to a variable an object that implements the command we want. We then use this object to perform the correct action. You have seen a similar idea in the various Listener interfaces. You implement a method such as actionPerformed or mouseClicked and register an object containing that method with a Listener.)

OK, let's go back over the full sequence of actions that leads up to an object being deleted:

  1. The user clicks the Delete button.
  2. The actionPerformed method of the DeleteButtonListener object that listens to that button is called.
  3. actionPerformed makes the instance variable cmd reference a DeleteCmd object. (You have to add code to actionPerformed so that this happens.)
  4. The applet waits for some other user-initiated action to occur.
  5. The user clicks somewhere in the canvas.
  6. The mouseClicked method of CanvasPanel is called.
  7. mouseClicked picks up the current mouse position and calls executeClick on the DeleteCmd object now referenced by cmd. The call to executeClick is given two parameters: the mouse position and a reference to the Drawing.
  8. executeClick finds the frontmost object in the drawing that is under the mouse position and, if there is such an object, removes it from the drawing. (Again, you have to write code in executeClick to make this happen.)
  9. After executeClick returns, actionPerformed calls repaint. The applet window is repainted, now minus the deleted object.

Note that since dragging means nothing in the context of having clicked the Delete button, the DeleteCmd class definition can just use the default empty-body definitions of executePress and executeDrag. The only method that needs to be written in the DeleteCmd class definition is executeClick.

You will need to write subclasses of Command to do the right thing for the various commands upon mouse click, press, and drag events in the canvas. You'll find that you can use the default empty-body method definitions provided by Command for some, but not all, methods in each command subclass.

The delete command is a particularly simple one. There's really nothing that needs to be remembered by the delete command between events. In contrast, consider the exchange command, which works on two objects that are clicked in succession. This command needs to remember whether an object has already been clicked and, if so, which object. To give you some idea of how to write a command that needs to remember information, I have provided the full implementation of the exchange command in ExchangeCmd.java.

First, notice that an ExchangeCmd object has an instance variable firstShape, which references a Shape. As is true for any reference variable, it is null initially.

Now, let's see what happens when a mouse click occurs in the canvas once the Exchange button has been clicked. First is a call to dwg.getFrontmostContainer, where dwg references the Drawing object. One of the methods of Drawing is getFrontmostContainer, which returns the frontmost Shape in the drawing that contains a given Point. The Point given to getFrontmostContainer is referenced by executeClick's parameter p. The call to getFrontmostContainer returns either null, if no object in the drawing contains the Point, or a reference to the frontmost object in the drawing that contains the Point. In either case, we save the reference returned from getFrontmostContainer in the local variable s. If this reference is null, then we forget about it and just return. Otherwise, we proceed.

The way we know whether a click is on the first or second object of a pair depends on the instance variable firstShape. We will adopt the following rule:

If firstShape is null, then the next object clicked is the first object of the pair. Otherwise, the next object clicked is the second object of the pair.
Therefore, the body of executeClick checks to see whether firstShape is null. If it is, then the object just clicked is the first one in the pair, and we just save it in the instance variable firstShape. Since firstShape is an instance variable, it will be remembered the next time that executeClick is called. (Actually, there's a little subtlety here. If you click the Exchange button after clicking the first object but before clicking the second object, a new ExchangeCmd object is created and used, and its firstShape instance variable is set to null. The result is that we won't remember the first object clicked. But this scenario occurs only if you click the Exchange button after clicking just the first object. It is in fact what we want. Suppose you clicked on a first object, clicked on the Delete button and deleted that first object from the drawing, then clicked the Exchange button again. If the object you next clicked on were treated as a second object, then it would try to exchange this second object with an object that had already been deleted.)

Now let's see what happens upon the clicking the second object. At that time, firstShape is not null, since it references the first object clicked. Thus, we fall into the else part. We call the getCenter method on the two objects, which are referenced by the instance variable firstShape and the local variable s. We then call the setCenter method on both objects, giving each the center of the other. That causes them to exchange their centers. Finally, we set firstShape back to null, so that the next object clicked will be taken as the first object of a pair. (Think about what might happen if we did not set firstShape back to null.)

Seeing the ExchangeCmd class should help you write the other command classes, such as MoveCmd, which drags objects. Here, when the mouse is pressed, it will need to identify the object, if any, to be dragged. And it will need to remember where the mouse was the last time during the dragging operation so that it can move the object by the appropriate amount. A MoveCmd object, therefore, might have instance variables telling it which Shape is being moved and where the mouse was the last time during the dragging operation. You can set and/or refer to them in any of the methods of MoveCmd. Similarly, when dragging out a new object in the drawing, say a rectangle, your AddRect object might want to remember the first corner of the object in an instance variable.

If you think about it a little, you will find that you do not need separate classes for each of the color-changing commands. They all do the same thing, just with different colors. In other words, I defined a single class, ColorCmd, that handles the Red, Green, and Blue buttons.

Once you get the hang of programming this way, you will probably come to think it's pretty cool. I know that I was impressed the first time I realized that my program could figure out how to execute a command without any switch statements or if ladders.

The Drawing class

The one big thing we haven't discussed is the Drawing class. This is your biggest design challenge. I specify three methods below that you must implement, because code that I supplied requires it. You should decide what other methods you need and implement them.

It is probably easiest if you start by writing all of the methods to handle button actions to see what sorts of things they need the Drawing class to do. Create method headers for methods that are needed. After you know all of the needed methods, figure out how to implement them.

I will explain how I organized the instance variables in my Drawing class. You can organize things differently if you like.

My implementation uses an ArrayList instance variable to store the Shape objects that are in the drawing. Because I know that all I'll ever add to the ArrayList are references to objects in subclasses of Shape, I used an appropriate generic type with my ArrayList.

My ArrayList is organized so that the frontmost object is at index 0 of the ArrayList and objects appear in order from front to back. That way, when I'm searching for the first shape that is under a given position, I can progress in increasing order through the ArrayList.

My Drawing class also has an instance variable that records the current default color.

You are required to implement three specific methods in your Drawing class—a constructor, draw, and getFrontmostContainer—specified exactly as given below. That's because the code I have provided assumes that they exist.

Extra Credit

This assignment has the opportunity for lots of extra credit. If you're adding functionality, you'll have to add to the GUI. Chances are that you'll see how to do it from looking at Editor.java.

You'll also find that you have to add new methods to some of the existing classes, as well as adding new classes.

Supporting additional shapes
If you look at the methods of the Graphics class, you'll see that you can draw polygons, rectangles with rounded corners, arcs, and text strings. The hard part of adding any of these is writing the containsPoint method. Also, if you choose to support text strings, you'll need some way to allow the user to enter the string. (Use the JTextField component.)

Please note that additional shapes such as Square and Circle, which are just specializations of the shapes in the basic editor, are not worthy of extra credit.

Copy button
In this variant of Move, when you start dragging a shape, it makes a copy of the shape, and the copy is what gets dragged. The original stays where it is. In terms of the front-to-back order, the copy should be immediately to the front of the original, rather than at the front of everything. That way, you can get the copy in front of the original, or you can get it in front of everything by then using the Front button.

Reshape button
Here, when you start dragging a shape, you change its actual shape. If you drag on one endpoint of a line segment, only that endpoint moves. If you drag on one corner of a rectangle or ellipse, the opposite corner stays put and the dragged corner moves. You should figure out what to do when the user drags on a shape but not near enough to an endpoint or corner.

Gridding and snapping buttons
Allow the user to toggle whether an evenly-spaced grid is displayed. (Implementation-wise, remember that the lines of the grid are not user-editable shapes.) Allow the user to change the grid spacing.

Allow the user to toggle whether grid snapping occurs. When grid snapping is on, whenever a new shape is added to the drawing, its coordinates snap to the nearest grid point. For example, if the grid spacing is 50 pixels and a rectangle is dragged out with corners at (40, 110) and (130, 180), then the rectangle that's added has corners at (50, 100) and (150, 200). Even better is to snap as the rectangle is dragged out, so that the adding processes is truly WYSIWYG (What You See Is What You Get). When snapping is on and a shape is moved, the amount that it's moved by must be a multiple of the grid size in each dimension. So as the object is being dragged around, it jumps in multiples of the grid size.

Undo button
Allow the user to undo the last change to the drawing. (Count all consecutive executeDrag calls on a given shape as one change.) This command is a little tricky, as you'll have to bear in mind that a change to the drawing could be from adding a shape, deleting a shape, moving a shape, changing a shape's color, moving a shape to the front or back, or exchanging the centers of two shapes. A change to the drawing might not change any shapes on their own, but just their relation to each other.

For even more extra credit, allow undo of multiple changes.

Redo button
If you implement the Undo button, consider implementing a Redo button, which redoes the most recently undone change. You can think of Undo-Redo as a stack, where Undo "pops" the effect of the most recent change and Redo "pushes" it back on. If you support multiple Undos, therefore, a sequence of 6 Undos followed by 4 Redos would be the same as a sequence of 2 Undos. Redo makes sense only after an Undo or Redo; once any other change to the drawing occurs, Redo is meaningless.

You can also come up with other interesting ideas.

Please hand in any extra credit as a separate program. That way, if for some reason it prevents the basic part of your program from working correctly, you won't be penalized for it.

Blackboard submission

  1. Submit via Blackboard the zip file of a folder (named, say, My Lab 3) that contains the screenshots and ALL java files, including the ones I supplied to you, This makes it easier for section leaders to run your applet.

    If you did extra credit, submit a second zip file, which is the zip file of a folder (named, say, My Lab 3 Extra Credit) that contains ALL java files needed to run your extra credit solution. This is to make sure that the extra credit, in case it does not work correctly, will not penalize your grade for the basic part of the assignment.

    If you have a partner, each of you should submit via Blackboard. When you submit, make sure that in the comments section, you state the name of your partner.

  2. Include at least three screenshots of the applet window. We should see that you have added a rectangle, an ellipse, and a line segment somewhere in the sequence. We should also see that you have moved an object, deleted an object, moved an object to the front, and moved an object to the back. Make sure that we can tell the order of your window shots, and write down the sequence of commands that got you to each shot from the previous one.

  3. If you realize that you need to change some of your code after you've submitted, you can resubmit by following the above procedure. Once you start to resubmit, finish the process. If you abandon resubmitting in the middle of the process, Blackboard tends to show us that you did not submit. Furthermore, if you resubmit, include all of your .zip files, not just those that differ from the previous submission. We will look at only your last submission.

Grading rubric

Total of 120 points

Correctness, Efficiency, and Elegance (80 points)

5Complete listeners in Editor
5Rect code
5Ellipse code
5Segment code
10Code to add an Ellipse, add a Segment, and add a Rect
5Handle Back command
5Handle Color commands
5Handle Delete command
5Handle Front command
5Handle Move command
25Drawing class

Structure (20 points)

10Good decomposition into objects and methods.
4Proper used of instance and local variable
2Instance variables are private
4Proper use of parameters

Style (15 points)

2Comments for classes
5Comments for methods (purpose, parameters, what is returned) in JavaDoc form.
5Good names for methods, variables, parameters
3Layout (blank lines, indentation, no line wraps, etc.)

Testing (5 points)

5At least 3 screen shots, along with the list of commands that produced them

Extra Credit

10Additional shapes (10 per shape)
10Copy button
15Reshape button
5Grid button
10Snapping button (requires Grid button)
20Undo of previous change
20Undo of arbitrary number of changes (so 40 total for full Undo)
15Redo of previous Undo (requires Undo)
25Full Undo/Redo for arbitrary number of changes (so 40 for full Redo)
?Other interesting additions