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
fillOvalmethod to draw ellipses. (Geometrically, the shape really is an ellipse, but the AWT calls the methodfillOval, notfillEllipse.) - 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 theDrawing 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
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:
- In Ellipse.java, the method
pointInEllipsetakes as parameters aPointand the bounding box of an ellipse, and it returns a boolean indicating whether thePointis in the ellipse. (On the boundary counts as "in" the ellipse.) You don't have to understand how this method works if you don't want to, but it uses basic geometric calculations. - Segment.java contains two
helper methods. They are there, in part, because you can't
click exactly on a line segment. In practice, if you click
within a "tolerance," that's good enough. I find that a
tolerance of 3 pixels is about right.
-
almostContainsPointis given aPoint, a bounding box in terms of itsleft,top,right, andbottomcoordinates, and atolerance. This method returns a boolean indicating whether thePointis withintoleranceof the bounding box. For example, suppose that the bounding box hasleft= 10,top= 20,right= 40, andbottom= 60, and lettolerancebe 3. Then (7, 17) is withintoleranceof the bounding box, as is (43, 63). For that matter, any point on or within the bounding box is OK, e.g., (10, 20) or (12, 22). But (6, 17) is not withintoleranceof the bounding box. -
distanceToPointis given aPointand the endpoints of a line segment, and it returns the distance from thePointto the closest spot on the infinite line that contains the line segment.
You should use these helper methods of
Segmentwhen determining whether aPoint, presumably from a mouse position, is close enough to a line segment that we consider the line segment as having been clicked or dragged. Consider thePointto be close enough if it is both within the tolerance of the line segment's bounding box and if the distance between the point and the infinite line containing the segment is within the tolerance. -
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
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:
- The user clicks the Delete button.
- The
actionPerformedmethod of theDeleteButtonListenerobject that listens to that button is called. -
actionPerformedmakes the instance variablecmdreference aDeleteCmdobject. (You have to add code toactionPerformedso that this happens.) - The applet waits for some other user-initiated action to occur.
- The user clicks somewhere in the canvas.
- The
mouseClickedmethod ofCanvasPanelis called. -
mouseClickedpicks up the current mouse position and callsexecuteClickon theDeleteCmdobject now referenced bycmd. The call toexecuteClickis given two parameters: the mouse position and a reference to theDrawing. -
executeClickfinds 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 inexecuteClickto make this happen.) - After
executeClickreturns,actionPerformedcallsrepaint. 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:
IfTherefore, the body offirstShapeisnull, then the next object clicked is the first object of the pair. Otherwise, the next object clicked is the second object of the pair.
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.
- A constructor, which creates an empty drawing with an initial
default
Colorgiven as a parameter. Theinitmethod of theEditorclass in Editor.java calls it. - A method
drawthat, given a reference to aGraphicsobject, has eachShapein the drawing draw itself. ThepaintComponentmethod of theCanvasPanelclass in Editor.java calls it. - A method
getFrontmostContainerthat, given a reference to aPoint, returns the frontmostShapein the drawing that contains thePoint, ornullif noShapecontains thePoint. TheexecuteClickmethod ofExchangeCmdcalls it.
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
Graphicsclass, 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 thecontainsPointmethod. Also, if you choose to support text strings, you'll need some way to allow the user to enter the string. (Use theJTextFieldcomponent.)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.
- 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
executeDragcalls 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.
- 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
- 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.
- 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.
- 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)
| 5 | Complete listeners in Editor |
|---|---|
| 5 | Rect code |
| 5 | Ellipse code |
| 5 | Segment code |
| 10 | Code to add an Ellipse, add a Segment,
and add a Rect |
| 5 | Handle Back command |
| 5 | Handle Color commands |
| 5 | Handle Delete command |
| 5 | Handle Front command |
| 5 | Handle Move command |
| 25 | Drawing class |
Structure (20 points)
| 10 | Good decomposition into objects and methods. |
|---|---|
| 4 | Proper used of instance and local variable |
| 2 | Instance variables are private |
| 4 | Proper use of parameters |
Style (15 points)
| 2 | Comments for classes |
|---|---|
| 5 | Comments for methods (purpose, parameters, what is returned) in JavaDoc form. |
| 5 | Good names for methods, variables, parameters |
| 3 | Layout (blank lines, indentation, no line wraps, etc.) |
Testing (5 points)
| 5 | At least 3 screen shots, along with the list of commands that produced them |
|---|
Extra Credit
| 10 | Additional shapes (10 per shape) |
|---|---|
| 10 | Copy button |
| 15 | Reshape button |
| 5 | Grid button |
| 10 | Snapping button (requires Grid button) |
| 20 | Undo of previous change |
| 20 | Undo of arbitrary number of changes (so 40 total for full Undo) |
| 15 | Redo of previous Undo (requires Undo) |
| 25 | Full Undo/Redo for arbitrary number of changes (so 40 for full Redo) |
| ? | Other interesting additions |