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 child components in Swing.

This assignment is to build the "Page Sorter" window for the Notebook application. This window will contain shrunken thumbnail components for each page in your notebook. You can click on these thumbnails to go to the selected page in the notebook, and drag them to rearrange them the order of the corresponding pages in the main application.

The learning goals for this assignment are:

Description

In this homework, we'll create the Page Sorter window that gives the user an overview of all of the pages in the Notebook application. The basic idea is that this window displays a scrollable view of page thumbnails -- shrunken representations of the pages in your notebook. The Page Sorter window should stay up-to-date with respect to the pages in your notebook, meaning that new pages should show up immediately in the Page Sorter, deleted pages should disappear, and any changes to individual pages should be reflected in the thumbnail that corresponds to that page.

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

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 PageThumbnail or something like that) and a new UI class (such as BasicThumbnailUI). These will work 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 any Components registered with it 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 pages 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).

Hint: Pages in the original notebook view can be arbitrary size, depending on how large you resize the window. How should you set the size of your thumbnails? There are two strategies you can use, and either one is fine by me.

Strategy #1: First, you can simply choose a small fixed size and paint however much of your scaled down Notepage contents you can fit into the thumbnail. You'll want to set your scale factor and your thumbnail size so that for the typical page size, you'll be able to render most of the content into its thumbnail.

Strategy #2: Second, you can get the actual size of the Notepage component that corresponds to the thumbnail, and make the thumbnail a corresponding fraction of that size (20% as big, if you're using a 0.2 scale factor for instance). If you do this, be sure to realize that your Notepage won't actually have a size until it's been layed out, so watch out for zero-sized Notepages. Also, if the Notepage is resized you'll need to detect this and change the size of your thumbnail. Do this by adding a listener to receive ComponentEvents from the original Notepage component--those will be sent whenever the component changes size.

Doing the Page Sorter Itself

Once you've got the thumbnails working you're set to do the Page Sorter 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 page sorter each time a page is created. Since these thumbnails are components, and since they'll be normal children of the page sorter, the application should call add() on the page sorter component to add these as children (note that this is the same method that normally gets called to add children to any component).

Your page sorter 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 PageSorter 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 Page Sorter 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 Page Sorter'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 page.

For the drag, when the mouse is pressed anywhere in the sorter 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 pages in the application to reflect the new ordering, and (2) recompute new standard positions for all of the thumbnails in the page sorter (so that everything appears aligned on a grid for example).

Hint: Your page sorter 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 page sorter, not just thumbnails. If you want to restrict this, you can define your own add() method on the page sorter that verifies the type of components passed to it and then calls super.add().

Hint: You may have noticed that the ordered list of pages you're keeping in your main application is much like a Model for the Page Sorter. If you wanted to get all fancy, you could architect the entire application so far with a model that contains all of the pages in the notebook, and then do separate Component and UI classes for both the main app and the page sorter. 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 Page Sorter 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 Page Sorter, after all, that is providing the selection and reordering functionality for the Notebook application, not individual thumbnails. You might, for example, want to reuse the thumbnail class in a window other than a page sorter, 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 PageSorter 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 PageSorter's paintComponent() method and simply draw the effect at a size slightly larger than the thumbnail. Since thumbnails are painted after the sorter 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 page sorter. 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 page sorter listen for updates to component sizes in order to correctly redraw the sorter 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 sorter will figure out where it should go in the new page order, 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 this assignment is significantly more difficult than the previous one, most of the parts should be fairly straightforward. Start with the basic PLAF component architecture and get that working in your application first; one easy way to do this is to implement the UI class's paint method so that it only draws the page background. This way you'll be able to see your component show up and know it's working correctly. After this do the stroke processing (it's the next easiest), followed by the text processing (slightly more complicated). Finally, once all of this is working, it should be fairly straightforward to keep a list of notepage components in your application and make the New/Delete/Next/Previous controls work.

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 "java NotebookApp" when run from inside your project directory (described below). 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 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 we migiht need in order to run your code.

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

Please take care to remove any platform dependencies, such as hardcoded Windows path names or depencence 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 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.