I created three classes:
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, redval,
up or down according to whether
redshift is positive or negative, in a proportional manner.
I chose to do this using a weighted average of redval 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
redval = (1-redshift)*redval + redshift * 255;(Using (1-redshift) as the coefficient of redval ensures that no change occurs when redshift==0; this is different from the way I did it in the sample Redden code.) When redshift is negative, we set redshift = -redshift (making it positive) and take the weighted average of redval and 0 (instead of 255 above):
redval = (1-redshift)*redval + redshift * 0;which is just (1-redshift)*redval.
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:
redval = (1-redshift)*redval + redshift * redbase;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.
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.
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.
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
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.
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.