HOMEWORK 2: Building Custom Swing Components
This is an INDIVIDUAL assignment.
Objective
In the last assignment you learned how to put together Swing applications
by using existing components. In this assignment, you'll learn how to
create your own Swing component from scratch. We'll be building the
"content area" of the photo album application that displays a single
photo as well as annotations added by the user. This PhotoComponent
you'll write will have to
respond to both keyboard and mouse input (using the mouse to simulate a
pen), and render appropriate output.
The learning goals for this assignment are:
- Experience with the Swing drawing pipeline.
- Experience writing a variety of input listeners.
- Experience with the Swing component architecture
Description
Don't be intimidated by the length of the homework description!
Although long, most of this writeup is here just to provide detail about
what I expect from this assignment, as well as some hints about how best
to do the implementation.
In this homework, we'll create a custom Swing component that serves as the
content area of the photo album application. The basic idea is that this
component displays a single photo, and also provides a way to store and
render annotations including text and drawn strokes. You can "flip the photo
over" to annotate on it using either mouse strokes or text.
There are a number of specific requirements for this assignment:
#1: Basic component architecture.
You should start by creating a new subclass of JComponent, called PhotoComponent
or some such. Your new component should have a paintComponent() method in it
that will do all the drawing, plus contain whatever state information is
associated with the photo. This will likely include a reference to the Image that
represents the photo, a boolean indicating whether it is in its "flipped" state or
not, plus a representation for any annotations on the photo (described later). It will
implement the necessary listeners (and add these listeners) so that it can respond
to input events.
#2: Photo display.
Your component will display a single photo, and so your component should render
this image in its paintComponent() method whenever it is requested to draw itself.
But be sure to realize that the component may be
larger or smaller than the photo it displays (because the user may resize
the window, for example). Your component should have a size and a preferredSize that are the size
of the photo itself, but you probably don't want any minimumSize or maximumSize. When your PhotoComponent is initialized, and before any photo is loaded, you probably want to use some default value for size and preferred size
So that the component looks good in cases where the window is larger than the photo itself,
you should render some nice-looking background behind the photo, which will be visible when
the photo doesn't cover the entire extend of the component. This can be as simple as a
solid color, or some pattern such as graph paper. You'd create this effect by simply doing
some drawing in your paintComponent() method before drawing the image.
If the user shrinks the window so that the component cannot display the entire photograph,
it should be scrollable so that the user can pan around the image. The easiest way to do this
is to simply insert your PhotoComponent into a JScrollPane, and then insert that JScrollPane into
the container area in your application. There are settings on JScrollPane that determine whether
the scrollbars are always displayed, or just when they are needed (it's fine if the scrollbars
are always displayed). Remember that the way JScrollPane works is that it allows its child to be any size it wants, but clips and positions it so that the correct part is shown under the visible region. This is why you want to make sure your component has a size and preferred size. If you reload a photo and change the size, you will probably want to call revalidate() on your PhotoComponent so that the scroll pane "notices" that its size has been updated.
If you want, you might also like to display some graphical "frame" around the picture, such as
a while border or "scrapbook corners," although this is not required.
#3: Flipping to annotate.
With physical paper photos, people often
flip them over to write notes and other annotations on the back. Your
PhotoComponent should support this also. Double-clicking on the photo
should cause it to be replaced by a plain white surface of the same size as the photo for annotation.
The background of your component should be drawn the same--only the photo should be replaced. When in this mode, you can
annotate the photo's "back" via drawn strokes and typed text (see #4 and
#5 below). Double-click on the photo back again to flip it over and see the
photo again. The current "flip state" of a given photo should be stored in a boolean
in your component.
Hint: Your paintComponent method will have two paths through it,
depending on the setting of this boolean. In the default path, it will
draw the background and then the image. In the flipped path, it will
draw the background, draw the white surface, and then draw the annotations
(see below)
#4: Support for drawn strokes.
When in the flipped state, you
should be able to draw onto the photo back using the mouse (if you're using a pen
tablet, the pen also produces mouse events and so this sort of implementation
will also work nicely on a pen-based computer). What this means is that
the user should be able to draw freehand strokes by dragging the mouse
on the photo back with the button pressed. The component should show the
stroke while it is in the process of being drawn, to give appropriate
feedback to the user. Drawing should only occur in the white back-of-photo area,
not the background.
Hint: Remember that you'll need to redraw all of these same
strokes anytime the Swing repaint pipeline tells you that you need to
paint your component. So, the classic way to do this is to add strokes
to a display list that contains the things to be rendered, and
then in your paint code you simply iterate through the items to be
painted, rendering them to the screen.
Hint: Painted strokes will look much better if you use
Java2D's anti-aliasing mechanism. Look at the setRenderingHints()
method on Graphics2D.
#5: Support for typed text.
When in the flipped state you should also allow typed text to be entered onto the photo back.
The way this should work is that the user clicks on the photo back to set an
insertion point for the text. Then, any typing will begin to fill
the photo back starting at that insertion point. Clicking again will reset the
insertion point to another position. While you don't have to do
any especially fancy text processing (no ligatures or custom fonts or
anything like that), you should make the basics work correctly. The
basics are:
- You should implement word wrap. This means that when your typing
hits the end of the photo back you should find the rightmost whitespace in the
line, break the line there, and put the remaining word on a
new line. If there is no whitespace in the line then you can just break
the line at the last character that will fit on the line.
- Use reasonable linespacing. Remember: ascent + descent + leading.
Please do not implement this feature by trying to insert a JTextComponent
into your photo back. I'd like you to get experience with the low-level Java
text APIs, plus reusing a photo component in this way is probably more work
than just doing it "by hand."
You do not have to implement more complicated features (although
you're welcome to for extra credit). For example, you do not have
to implement:
- Backspacing over already entered characters.
- The ability to put the insertion point into already-entered text and
edit it.
- The ability to select a span of text, or do copy-and-paste, etc.
Hint: While all the word wrapping stuff may seem
difficult, it's actually pretty straightforward to implement. The key is to remember that, like
with stokes above, you'll need to keep a data structure containing the
text that will be rendered by your component.
So one way to architect things is to
simply create a new object to hold a text block whenever the insertion
point is set; this object only needs to remember the insertion point and
the set of characters entered at that point.
Whenever characters are typed they are simply added
to the current text block object. The job of your paint code, then, is
simply to iterate over the list of text blocks and draw them to the
screen, wrapping as you draw based on the size of the photo back.
Hint: Telling the difference between "text mode" and "draw mode"
should be easy: If you see a mouse down followed by movement, you can
assume you're drawing. If you see a mouse click (press followed by
release), you can assume you've
set the text insertion point for keyboard entry.
Hint: If you find yourself needing to do fancy FocusManager stuff,
you're probably working too hard. You should just be able to add
KeyListeners to your component, and call setFocusable(true) on it.
#6: Integration with the rest of the application
Once you've implemented and debugged your PhotoComponent, it's time
to integrate it into the application you wrote for Homework #1.
Important Note: my recommendation is to not use full-size digital images when
testing your application. Images take a lot of memory, and without additional
configuration settings when you run Java, your Java Virtual Machine may run
out of memory (this will especially be a problem when we start handling multiple
photos in the next assignment.) So, to make your life easier, you may want to scale
down your photos to 1024x768 or some such.
When your application starts up, it should contain no photos, and hence no
PhotoComponents.
Selecting Import should prompt the user for a photo file (via the JFileChooser),
and create a new PhotoComponent containing the selected photo and display it in the content area.
NOTE that for this assignment, we'll only be dealing with a single photo. So you don't
need to worry about being able to select a folder and importing a bunch of photos... it's
ok if your program just pops up an error message or ignores any selection that's not
a single file.
The currently displayed PhotoComponent
should be used in the central content area of your application, should
resize properly when the window is resized, etc., as described above. (You don't have to
worry about doing fancy stuff like scaling
photos.)
Selecting Delete Photo should delete the current PhotoComponent, meaning that
the application should just show an empty content area.
Selecting Previous and Next will do nothing, since there's only ever zero or one
photos in this version of the homework.
Changing any of the View Modes will do nothing, since we're currently only ever displaying
zero or one photos.
Extra Credit
There are a lot of ways to augment this assignment, should you choose
to. Some obvious ways are:
- More text and graphics processing
- Allow editing of already-entered text, such as insertion or backspace. Up to +4 points, depending on features implemented
- Customizable fonts. Either on a photo-wide basis (+1 point), or for
individual spans of text (+3 points).
- Pen sizes and ink color: +2 points.
- Additional drawing tools, such as elipses or rectangles: Up to +4 points, depending on what's implemented.
- Fancy features
- Saving and loading of data. PhotoComponents should remember what picture
they're displaying as well as the strokes or text on them, between
runs of the application. It'd be cool if this happened
automatically as strokes or characters are entered, or photos are loaded, so you never have to
remember to hit "save." Up to +4 points.
- Scaling photos. Provide a control to let the user scale the displayed photo up or down. +2 points
- Other stuff. If you build in some particularly neat feature, make
sure you let us know about it when you turn it in and we'll consider it
for possible extra credit.
Deliverable
See here for instructions on how to submit your homework. These instructions will be the same for each assignment.