Comp 170 project 2: The ShiftColor Dialog

The other filters all involve pre-set effects that do not involve user input. The ShiftColor filter (I called it ShiftFilter) is a bit like the Redden filter discussed before, but it first prompts the user for "color shift" percentages for each of the three colors. Positive percentages increase that color in each pixel; negative percentages decrease the color. The way we prompt for the shift factors is to put up a new window (called a "dialog box") with three sliders. The user sets each slider to be a number from -100% to 100% by dragging, and then clicks the "Done" button. Because the dialog box blocks access to the main application until the user is finished with it, it is called a modal dialog: modal because it defines an input "mode" that behaves completely differently from the application otherwise.

I created three classes:

The code for Shifts.java is here.

The Shift Filter

This is mostly a normal filter. We get the Shifts object from the user by calling the dialog box:
            shifts = (new ShiftDialog(parentFrame)).getShifts();
This returns when the user clicks "Done" in the dialog box (just how this happens is described below), and the dialog goes away.

After you get the shifts object, you extract the shift values:
      double redshift = shifts.getRed(); // same for green, blue
You then need to shift each pixel's red value, red, up or down according to whether redshift is positive or negative, in a proportional manner. I chose to do this using a weighted average (with weight = redshift) of red and 255 (for increases) or 0 (for decreases); this does a more-or-less proportional shift and guarantees a color value in the range 0..255. If redshift is positive, this is

    red = (int) (red*(1-redshift) + 255*redshift);
When redshift is negative, we set redshift = -redshift (making it positive) and take the weighted average of red and 0 (instead of 255 above):
    red = red*(1-redshift) + 0*redshift;
which is just red*(1-redshift).

You can simplify the cases your program has to deal with a little by defining a variable redbase representing 255 or 0, the other end of the weighted average. If redshift≥0, redbase=255. If redshift<0, then redbase=0 and redshift = -redshift. Now, for each pixel all you have to do is:

    red = redval*(1-redshift) + redbase*redshift;
The performance improvement is slight; if you don't feel this helps, just do the checking for positive/negative redshift inside the double for loop; this is likely more "straightforward".

And don't forget to do the same for greenShift and blueShift.

There's one complication in the constructor to ShiftFilter: we need to pass the frame field of class ImageViewer as a parameter. This is because when ShiftFilter creates the ShiftDialog, above, the latter needs access to the parent frame (hence the parameter parentFrame above, which is the value of the ImageViewer frame field as passed in via the ShiftFilter constructor). Modal Dialogs need to "suspend" the parent frame until after they complete; hence they need to know the identity of the parent frame.

A complication to this is that the ShiftFilter object is created in class ImageViewer in the method createFilters(), which is called before makeFrame(). That won't do, as at that point frame is null. However, you can't reverse the order either, as makeFrame() requires that the menu list created by createFilters be already in place. You can either move the creation of frame (frame = new JFrame("ImageViewer")) from makeFrame() to a separate line in the ImageViewer constructor ahead of createFilters (this is what I did), or else move createFilters after makeFrame() and move the portion of makeMenuBar about creating menu items for the filters to somewhere else that can be called after createFilters. Isn't circularity fun?

Because of the JFrame reference, class ShiftFilter needs to import javax.swing.* in addition to the things every filter imports.

Sliders

Sliders are pretty straightforward. Creating them just involves
     redSlider = new JSlider(-100, 100, 0);
The -100, 100, and 0 are respectively the left end, right end, and where the "slider" is placed initially. (The idea is that 100 represents 100%; see below for where we implement that.)

For my sliders I also enabled "tickmarks", unnumbered every 5 spaces and numbered ones every 25 spaces:

        redSlider.setMajorTickSpacing(25);
        redSlider.setMinorTickSpacing(5);
        redSlider.setPaintTicks(true);
        redSlider.setPaintLabels(true);
(Actually I created a method to do this with theSlider as parameter, so I could write it only once and call it for all three colors.)

Sliders have an ActionListener-like interface called ChangeListener that informs the program of each change to the slider value, but you don't have to use that, since all you're concerned about is the final slider value when the modal dialog terminates. See below.

Creating Modal Dialogs

As stated above, the ShiftDialog class will be responsible for creating the auxiliary window that will contain the sliders, by which the user sets the color-shifting parameters.

There are built-in ways to create the "classic" modal dialog for user input: a message and a couple of buttons. However, given that we need three sliders, three labels for the sliders, and a Done button, we're going to have to do it from scratch. The class JDialog is used, instead of JFrame, but otherwise we sort of model the approach of the makeFrame method in class ImageViewer.

As indicated above, creating a modal dialog window requires providing the parent window (field frame of class ImageViewer) as parameter. Here's my JDialog creation:

        theDialog = new JDialog(parentFrame, "Color Tweaker", true);
The true parameter states that the new window is a modal dialog. If you set this to false, note that the application is still available when the dialog box is open. Note also that the dialog box now fails to work at all; can you figure out why?

After creation, the following steps are rather similar to those of makeFrame:

In addition to all that, there are a few dialog-specific steps you should perform. First, you want the dialog box to appear in roughly the same place on the screen as the original window (this is another reason for all that stuff above about passing the original JFrame as a parameter, though this one is optional):

        theDialog.setLocationRelativeTo(parentFrame);
Much more importantly, you do need a Layout Manager (things go rather badly if you omit it). Here's a one-liner for that:
    contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.PAGE_AXIS));
The PAGE_AXIS means that the labels and sliders are arranged vertically.

Getting the dialog results

Once your dialog finishes, how do you get the results back to the ShiftFilter? Recall the code from above:
            shifts = (new ShiftDialog(parentFrame)).getShifts();
The ShiftDialog constructor terminates when you call theDialog.setVisible(false). (Note that in effect Java waits for setVisible(true) and then setVisible(false)). To put it another way, the constructor blocks, or waits for the visibility to change before ending. (This is, I believe, specific to modal dialogs; other frames don't necessarily have the constructor block like that). At that point, the getValues() method is free to start, and does so; it retrieves the values and returns them without further blocking. Note that the getShifts() method doesn't do the waiting, the ShiftDialog constructor does.

Recall that I named the method called by the Done button's ActionListener finish(). What finish() does is to

  1. get the value of each slider and store them in shifts
  2. Do the setVisible(false) thingie.

To get the values, I used

        shifts.setRed(redSlider.getValue()/100.0);
Note that getValue() returns an int in the range -100...100; I convert to a double in the range -1.0..1.0 through the miracle of floating-point division. Because sliders only deal with int values, there is no way to create a slider that returns the double value directly. Note that this means you do not have to create any ChangeListener for changes to the sliders; you just check their state at the end.

What's still broken?

I never added a cancel button (although clicking on the "close" box works).

The off-center layout of the Done button isn't terribly satisfactory. For that matter, the entire layout could use improvement. The sliders could use more space around them; maybe they could even be enclosed in little boxes.