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 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
pointInEllipse
takes as parameters aPoint
and the bounding box of an ellipse, and it returns a boolean indicating whether thePoint
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. - 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.
-
almostContainsPoint
is given aPoint
, a bounding box in terms of itsleft
,top
,right
, andbottom
coordinates, and atolerance
. This method returns a boolean indicating whether thePoint
is withintolerance
of the bounding box. For example, suppose that the bounding box hasleft
= 10,top
= 20,right
= 40, andbottom
= 60, and lettolerance
be 3. Then (7, 17) is withintolerance
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 withintolerance
of the bounding box. -
distanceToPoint
is given aPoint
and the endpoints of a line segment, and it returns the distance from thePoint
to the closest spot on the infinite line that contains the line segment.
You should use these helper methods of
Segment
when 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 thePoint
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. -
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
actionPerformed
method of theDeleteButtonListener
object that listens to that button is called. -
actionPerformed
makes the instance variablecmd
reference aDeleteCmd
object. (You have to add code toactionPerformed
so that this happens.) - The applet waits for some other user-initiated action to occur.
- The user clicks somewhere in the canvas.
- The
mouseClicked
method ofCanvasPanel
is called. -
mouseClicked
picks up the current mouse position and callsexecuteClick
on theDeleteCmd
object now referenced bycmd
. The call toexecuteClick
is given two parameters: the mouse position and a reference to theDrawing
. -
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 inexecuteClick
to make this happen.) - After
executeClick
returns,actionPerformed
callsrepaint
. 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.
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
Color
given as a parameter. Theinit
method of theEditor
class in Editor.java calls it. - A method
draw
that, given a reference to aGraphics
object, has eachShape
in the drawing draw itself. ThepaintComponent
method of theCanvasPanel
class in Editor.java calls it. - A method
getFrontmostContainer
that, given a reference to aPoint
, returns the frontmostShape
in the drawing that contains thePoint
, ornull
if noShape
contains thePoint
. TheexecuteClick
method ofExchangeCmd
calls 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
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 thecontainsPoint
method. Also, if you choose to support text strings, you'll need some way to allow the user to enter the string. (Use theJTextField
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.
- 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.
- 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 |