HOMEWORK 3: More Custom Components and Layout

This is an INDIVIDUAL assignment.

Objective

In the last assignment you saw how to use Swing's MVC pattern to create custom components. In this assignment, you'll see how the MVC pattern can be exploited to create new views onto existing data models. You'll also learn how to manage and layout children components in Swing.

This assignment is to build the "Light Table" window for the Photo Album application. This window will contain shrunken thumbnail components for each photo in your album. You can click on these thumbnails to go to the corresponding photo in the album, and drag them to rearrange them the order of the corresponding photo in the main application.

The learning goals for this assignment are:

Description

In this homework, we'll create the Light Table window that gives the user an overview of all of the photos in the album. The basic idea is that this window displays a scrollable view of photo thumbnails -- shrunken representations of the photos (along with their annotations) in your album. The Light Table window should stay up-to-date with respect to the photos in your album, meaning that new photos should show up immediately in the Light Table, deleted photos should disappear, and any changes to individual photos should be reflected in the thumbnail that corresponds to that photo. For example, if a photo is flipped, the corresponding thumbnail should show up as flipped also.

Users should be able to do a number of operations on thumbnails in the Light Table:

See below for hints on how to do this, and more details about the specific features.

Doing the Thumbnails

Doing the thumbnails should be easy! We'll build on the MVC architecture you used last time, reusing the model from before. You should create a new Component class (called Thumbnail or PhotoThumbnail or something like that) and a new UI class (such as BasicThumbnailUI). These thumbnail classes will access the same data from your original model. They should display the photo (if the model is in the "unflipped" state) or the annotations (if the model is in the "flipped") state. You don't have to worry about rendering backgrounds or frames for the thumbnails--just the photo and annotations. This means that the thumbnail component will display the data from your original model in almost exactly the same as the Component and UI classes you did for the previous assignment with only a few exceptions: You can create both of these classes by basically just starting from a copy of your existing UI and component classes and then simplifying and making only minor tweaks. Because the model communicates with the Component using events, the model never even has to know that you've attached a new UI or component to it! Your thumbnails should automatically stay in sync with PhotoComponents as they're updated in the main view.

Hint: Scaling is easy. In fact, you can reuse the existing paint() code from your previous UI class with the addition of only a few lines: just make a copy of the Graphics object that's passed to your paint() method, cast it to a Graphics2D object (the one that has all the fancy operations on it), and then call scale() on it, passing a floating point scaling factor (0.2 to draw at 20% normal size, for example).

Doing the Light Table Itself

Once you've got the thumbnails working you're set to do the Light Table window. While there are a lot of ways to do this, here's how I'd suggest proceeding.

Making the Component
First, I'd create a custom component by subclassing JComponent. As this will be a fairly simple component, you do not need to use the MVC pattern for it (although you're welcome to if you want to).

Managing Children
Your main application should add thumbnails to the Light Table each time a PhotoComponent is created. Since these thumbnails are components, and since they'll be normal children of the Light Table component, the application should call add() on the Light Table component to add these as children (note that this is the same method that normally gets called to add children to any component).

Your Light Table component should "notice" any change to the set of thumbnails it contains; soliciting ContainerEvents is the easiest way. When new thumbnails are added or old thumbnails are removed, the Light Table should begin a process that recalculates its preferredSize based on the current set of thumbnails. This size should be the insets around the component, plus the room taken up by any thumbnails, plus whatever spacing you have around the thumbnails. Then, you should iterate through the thumbnails and call setBounds() on each of them to update their position in the new layout. Important: If you change your preferredSize, you have to tell Swing about this fact because the sizes and positions of other components in your frame may need to change. You do this simply by calling revalidate() and then repaint() whenever your size changes; Swing takes care of the rest.

Responding to Events
You'll need to respond to MouseEvents and MouseMotionEvents in order to implement the interaction with the user. To handle the double click, your Light Table should listen for MouseEvents; when mouseClicked() is invoked look at the clickCount field in the event you get to determine if it's a double click or not. If it is, you then need to find out which thumbnail was clicked on. Iterate through the list of the Light Table's children (use getComponents()) and simply ask each one if the event was inside the bounding box of the component (using something like child.getBounds().contains(event.getPoint()) for example).

When you know what thumbnail has been selected, you can call back to the application to have it set the current photo.

For the drag, when the mouse is pressed anywhere in the light table you should record the thumbnail (if any) on which the press occurs. Then, when a mouse drag begins, you can begin moving the selected component. Do this by calling setBounds() on the thumbnail. Note that to do this "right" you'll have to set the new location of the thumbnail to be the location of the mouse pointer minus the offset of where the mouse press occurred inside the thumbnail.

When the drag ends (and the mouseReleased callback is fired), you should iterate through the list of thumbnails to figure out where the dragged thumbnail was dropped. Then you can (1) reorder the photos in the application to reflect the new ordering, and (2) recompute new standard positions for all of the thumbnails in the LightTable (so that everything appears aligned on a grid for example).

Hint: Your Light Table can use the normal component methods to access the list of child thumbnails contained inside of it. So, for example, it can use getComponents() to get a list of all current children. You may have noticed that there's the possibility for a caller to add any type of child to the Light Table, not just thumbnails. If you want to restrict this, you can define your own add() method on the Light Table that verifies the type of components passed to it and then calls super.add().

Hint: You may have noticed that the ordered list of PhotoComponents you're keeping in your main application is much like a Model for the Light Table. If you wanted to get all fancy, you could architect the entire application so far with a model that contains all of the PhotoComponent models in the album, and then do separate Component and UI classes for both the main app and the Light Table. No requirement to do this however.

Hint: Having repaint problems? Are your changes not getting reflected on screen? Be sure to call revalidate() anytime the set of children changes, or your size changes. For the thumbnails, be sure that you're repaint()'ing the thumbnail whenever the underlying model sends you a ChangeEvent.

Design rationale: In the description above, I have the Light Table doing the input processing, while individual thumbnails are "output only." You could also do this a different way, with thumbnails themselves soliciting the events that make them clickable, draggable, etc.

While each strategy has it's own merits, I think the one I've outlined above is the most sensible. It's the Light Table, after all, that is providing the selection and reordering functionality for the album application, not individual thumbnails. You might, for example, want to reuse the thumbnail class in a window other than a Light Table, and so selecting and dragging might have different semantics there. If the thumbnail class itself implemented this functionality, it would make thumbnails harder to reuse.

Possible gotcha: If you make the thumbnails respond to mouse events, then they'll consume any events that happen on them (remember that Swing dispatches events to the lowest leaf-node component that can accept those events). Be sure to not make your thumbnail component a MouseListener or MouseMotionListener if you don't want this to happen.

Design rationale: If you've done much Swing programming in the past, you know that Swing typically uses Layout Managers to handle the sizing and positioning of child components inside a container. Why are we not implementing this as a custom layout manager? The main reason is that I want to be able to drag and move thumbnails without having to compete with whatever positions that a layout manager would be trying to set for these. If you do want to implement a custom layout manager to do this, see me about getting some extra credit. :-)

Extra Credit

As usual, there are a lot of ways to make this fancier than described.

A simple bonus is to have the Light Table show the selected thumbnail differently somehow. For example, it may detect which thumbnail is selected and paint a "halo" or "glow" effect under it. The way you'd do this is to override the Light Table's paintComponent() method and simply draw the effect at a size slightly larger than the thumbnail. Since thumbnails are painted after the Light Table gets painted, the glow effect will be visible behind the thumbnail. +1 point for this one.

Another possibility is to do variable-sized thumbnails. One way you could do this is to have a "zoom slider" at the bottom of your Light Table. Moving the slider changes the scale factor--meaning you'd iterate through all of the thumbnails and update the scale factor that they use to paint and to compute their sizes. This should be a very cool effect. Remember that you'd have to have the Light Table listen for updates to component sizes in order to correctly redraw the Light Table itself; likewise, the thumbnails should notice when their scale factor is updated and then call repaint() on themselves and generate a component event when their size changes. +4 points for doing this one.

As described above, when a drag ends the Light Table will figure out where it should go in the new photo ordering, then recompute a standard "grid" layout for all of the thumbnails. Visually, this will look like all of the new thumbnails instantly snapping into their new locations. We know that jarring visual transitions are bad (right??), and so a nicer effect would be to smoothly move the thumbnails to their new positions. This actually isn't as hard as it sounds: look at the current position and the intended final position for each thumbnail. Compute a set of intermediate points (maybe 10 or 20) between these, for each thumbnail, and save these in a list. Now you're all set! Use the Swing Timer class (javax.swing.Timer) to schedule a series of events; for each event, set the location of each thumbnail to the successive item in its intermediate position list. By changing the position of the components, Swing should repaint the window and show each component smoothly moving to its new position on the screen. +4 points for this one.

As usual, if you do something else that's above and beyond the call of duty, let us know in your README file and we may assign some extra credit for it.

Deliverable

This is an INDIVIDUAL assignment; while you may ask others for help on Java or Swing details, please write the code on your own. While you can use any development environment you choose, you are not allowed to use "GUI builder" type tools (like JBuilder or Eclipse GUI Builder).

This assignment will probably be a bit easier for most people than the last one. My suggestion is to start with the thumbnails. See if you can get a simple thumbnail component going that you can display in a simple, standalone frame next to your main application before starting on the Light Table.

Once you've got this going, implement the Light Table component itself. See if you can get the basic layout mechanics going first, before starting on input. In other words, make it respond to new thumbnails being added, compute a preferred size based on the number of thumbnails, and then set the positions of these thumbnails based on the preferred size.

Finally, I'd save the input stuff for last. It's not hard, but does require that most of the rest of the UI is working correctly before you can start on it. To turn in the assignment, please follow the same process as last time:

0. Make sure your program is runnable from the command line using the command "j ava PhotoAlbum". Use exactly this name (with no package) to make things easier on me and the TAs.

1. Create a new directory using your last name as the name of the directory.

2. Compile your application and place both the sources and classfiles into this directory (they can be at the top-level or in a subdirectory, whatever you want).

3. Put a README.txt file (described below) into the top level of the directory. This file should contain your name and email address, the version of Java you used (1.5.x or 1.6.x, please) as well as any special info I might need in order to run your code (command line arguments, etc.)

4. ZIP this directory and submit it via T-Square (instructions are here).

If you do any extra credit work, be sure to let us know about it in the README file so that we don't miss it!!

Please take care to remove any platform dependencies, such as hardcoded Windows path names or dependence on a particular look-and-feel that may not exist on all platforms. Also, if you use any images in your application, please make sure that you include these in your ZIP file and that your code will refer to them and load them properly when from from inside the directory that's created when I unZIP your code.

Grading for this assignment, and future assignments, will roughly follow this breakdown:

Please let the TA or me know if you have any questions.