In this assignment, you will use inheritance in two ways to create a simple object-oriented graphical editor applet.
There will be no penalty, in terms of points, for working together on this assignment. Naturally, you will want to make sure that everything you submit has both your names on it. Please make sure that both of you submit the code electronically. It will be complicated for us to organize the grading, and to give you the grade you deserve, without your taking a moment to submit your work in this way.
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. We are asking you to create a .zip file of all your files and submit only the .zip file. That will make it easier for us to download your code, and it will also make it easier for you to submit extra credit separately from the basic part.
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:
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
.)
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
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.
Shape
hierarchyShape
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
:
pointInEllipse
takes as parameters a
Point
and the bounding box of an ellipse, and it
returns a boolean indicating whether the Point
is
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.
almostContainsPoint
is given a
Point
, a bounding box in terms of its
left
, top
, right
,
and bottom
coordinates, and a
tolerance
. This method returns a boolean
indicating whether the Point
is within
tolerance
of the bounding box. For
example, suppose that the bounding box has
left
= 10, top
= 20,
right
= 40, and bottom
= 60,
and let tolerance
be 3. Then (7, 17) is
within tolerance
of 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 within tolerance
of the bounding
box.
distanceToPoint
is given a
Point
and the endpoints of a line segment,
and it returns the distance from the Point
to the closest spot on the infinite line that contains
the line segment.
You should use these helper methods of Segment
when determining whether a Point
, 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 the Point
to 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.
Command
hierarchyCommand
. 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 "strategy pattern." Different objects contain
different implementations, or "strategies," 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 strategy 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:
actionPerformed
method of the
DeleteButtonListener
object that listens to that
button is called.
actionPerformed
makes the instance variable
cmd
reference a DeleteCmd
object.
(You have to add code to actionPerformed
so that
this happens.)
mouseClicked
method of
CanvasPanel
is called.
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
.
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.)
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:
IfTherefore, the body offirstShape
isnull
, 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.
Drawing
classDrawing
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 Draw
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.
Color
given as a parameter.
The init
method of the Editor
class in Editor.java calls it.
draw
that, given a reference to a
Graphics
object, has each Shape
in
the drawing draw itself. The
paintComponent
method of the CanvasPanel
class
in Editor.java calls it.
getFrontmostContainer
that, given a
reference to a Point
, returns the frontmost
Shape
in the drawing that contains the
Point
, or null
if no
Shape
contains the Point
.
The
executeClick
method of
ExchangeCmd
calls it.
You'll also find that you have to add new methods to some of the existing classes, as well as adding new classes.
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.
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.
As usual, if you come up with other interesting ideas, let me know in advance, so we can discuss how many points they're worth.
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.
Total of 120 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 |
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 |
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.) |
5 | At least 3 screen shots, along with the list of commands that produced them |
---|
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 |
Thanks to Tom Cormen who developed this lab for the CS 5 course.