You will submit your work for this mini project as homeworks 23, 24, 25, 26.
Your code (but not your name unless you choose to add it to the source files) will be visible to all classmates after each submission. You can study your classmates' code, and you can use any of it (with attribution) in the next phase of the project.
The purpose of the mini project is to develop a small scene editor that lets users add and rearrange shapes to a scene, and save their work.
Deliverable for each phase: A zip file with classes SceneViewer
, SceneComponent
, Shape
, the shape classes, a test input file, and any images that you are using. There is no CodeCheck. (CodeCheck doesn't work for GUI programs.)
There are three kinds of shape:
RectShape
draws or fills a java.awt.Rectangle
. It is constructed with the RGB color values for the shape color, and true
if the shape is filled or false
if it is not, followed by x, y, width, and height.TextShape
draws text. It is constructed with the font name and size, the text string, and the x- and y-coordinates of the top left corner. Text is always black. The font is always plain (not bold/italic).ImageShape
draws an image. It is constructed with the image file name and the x- and y-coordinates of the top left corner.Provide a superclass or interface Shape
with a method
void draw(Graphics2D)
Make a SceneComponent
that holds an ArrayList<Shape>
. Its paintComponent
method calls draw
on all shapes.
Your main
method should read a file with shapes and draw all of them. The file has a specific format. Each line starts with the shape class and is followed by the construction parameters, separated by commas. Example:
RectShape,255,0,0,false,10,20,100,50 TextShape,Serif,Hello World,30,200,200 ImageShape,dog.gif,0,200
Tip: The challenge in drawing a TextShape
is that you are given the top left corner, but drawString
uses the base point. You need to adjust by the ascent (see section 10.7).
Make it so that one can select, move, copy, and delete shapes.
Add methods
void translate(int dx, int dy) Rectangle getBounds()
to the Shape
type and its subclasses.
When the user clicks on the scene component, find the first shape whose bounds contain the mouse cursor, and draw “grabber squares” in the four corner points. When the user drags the mouse, translate the shape by the difference in mouse movement.
Add a button “Delete” that deletes the currently selected shape.
Add a button “Copy” that clones the currently selected shape and adds it to the drawing, translating it slightly. Make that the selected shape so that the user can easily drag it to the desired location.
Put those buttons in some convenient place.
Tip: Now the “model” of the scene component is an ArrayList<Shape>
+ the currently selected shape. Modify paintComponent
to draw the grabbers when a shape is selectd. Call repaint
on the SceneComponent
whenever you change something, just like in listing 11.5.
Tip: The mouse dragging is a bit fussy. You need to remember the old mouse coordinates when the mouse is clicked. When it is dragged, compute the difference between the old and new location, use that to translate the shape, and remember the new mouse location. The simplistic strategy in Listing 11.5 doesn't look as good because it makes the shape jump to the center.
Tip: Computing the bounding rectangle of a TextShape
requires you to compute the string bounds (see Section 10.7).
We want to be able to add new shapes. Make a text field and an Add button. The user types the same line as one would find in the file in the text area. Also make a Save button that saves the diagram. Simply add toString
methods to each Shape
subclass, using the same string format as for reading.
Next, we want more shapes. Instead of having all sorts of classes LineShape
, EllipseShape
, and so on, we'll use the java.awt.Shape
interface that is implemented by lots of shape classes. Note that Graphics2D.draw
and Graphics2D.fill
have parameters of type java.awt.Shape
.
Define a GeneralShape
subclass of our own Shape
type that has an instance variable of type java.awt.Shape
. The GeneralShape.draw
method calls Graphics2D.draw
or Graphics2D.fill
with that shape. The getBounds
method calls getBounds
on that shape.
You have to work a bit harder on translate
since Shape
has no translate
method. Instead, make it so that a GeneralShape
has an x- and y-coordinate that is updated by translate
. In the draw
method, call translate
on the Graphics2D
object, then call draw
or fill
, and then call translate
again in the opposite direction.
Finally, one needs to construct those shapes. We'll assume that the shape has a constructor that can take a sequence of integer and floating-point values. (They all do.) The text description contains the class name, in the format accepted by Class.forName
, and the construction parameters. This follows the color, the fill mode, and the translation offsets. For example:
GeneralShape,255,0,0,false,0,0,java.awt.geom.RoundRectangle2D$Double,10,20,100,50,5,5
Tip: Use Class.forName
to load the given class and call newInstance
with the remaining parameters. That's a bit tricky because you need to know whether to pass Integer
, Float
, or Double
values. Look through all constructors and find one that has the right number of arguments, all of which should be of type int.class
or double.class
. Then convert each string into an Integer
or Double
.
Tip: You need to be able to write the shape out again. It could have been translated. Save the color, fill mode, and translation offsets, followed by the description of the shape when it was constructed (which you should store).
Clean up the user interface. Instead of that silly text field, provide a combo box for the shape types next to the Add button and then pop up a dialog with the fields needed for the shape type. A color picker would be nice. Also add a menu with options File → Open and File → Save that pop up a JFileChooser
, and File → Exit that closes the program. If the user exits without having saved, ask if that's ok.