/* 
 * JGimp - A Java extension for the GIMP
 * Copyright (C) 2002  Georgia Tech Research Corp.
 * Written by Michael Terry, mterry@cc.gatech.edu
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package edu.gatech.cc.mterry.plugin;

import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JRadioButton;
import javax.swing.WindowConstants;

import org.gimp.jgimp.GimpApp;
import org.gimp.jgimp.JGimpBufferOverflowException;
import org.gimp.jgimp.JGimpData;
import org.gimp.jgimp.JGimpDrawable;
import org.gimp.jgimp.JGimpException;
import org.gimp.jgimp.JGimpImage;
import org.gimp.jgimp.JGimpInvalidDrawableException;
import org.gimp.jgimp.JGimpInvalidPixelRegionException;
import org.gimp.jgimp.JGimpLayer;
import org.gimp.jgimp.JGimpPDBArgTypeConstants;
import org.gimp.jgimp.plugin.JGimpImageTypeConstructor;
import org.gimp.jgimp.plugin.JGimpMenuPathConstructor;
import org.gimp.jgimp.plugin.JGimpParamDescriptorListBuilder;
import org.gimp.jgimp.plugin.JGimpPlugIn;
import org.gimp.jgimp.plugin.JGimpPlugInReturnValues;
import org.gimp.jgimp.plugin.JGimpProcedureDescriptor;
import org.gimp.jgimp.proxy.JGimpProxyException;
import org.gimp.jgimp.utils.JGimpImagePanel;
import org.gimp.jgimp.utils.JGimpMacros;

/**
 * This plug-in demonstrates how to write a Java-based plug-in that:
 * <ul>
 * <li> contains multiple plug-ins within the same class,
 * <li> copies pixel data to/from the GIMP,
 * <li> displays a UI to allow for user interaction (including a progress bar),
 * <li> displays a Java-based image preview, and
 * <li> remain resident.
 * </ul>
 * This plug-in desaturates an image in four different possible ways,
 * showing the result in a new image. The four ways are:
 * <ol>
 * <li> Extracting the red channel only
 * <li> Extracting the green channel only
 * <li> Extracting the blue channel only
 * <li> Displaying an average of the three channels in a grayscale image
 * </ol>
 * It also has a fifth plug-in which lets the user interactively choose between the four
 * different modalities.
 * <br>
 * The plug-in remains resident to save settings between invocations of its UI.
 * There may be better ways to do this naturally with the GIMP (that is, save settings)
 * which are less wasteful of memory.
 * <br>
 * Note: All the GUI code and desaturation code are intermingled to make it a bit easier
 * to see how everything can be put together into a Java-based plug-in. Your own plug-ins
 * might want to separate the interface from the underlying functionality
 */
public class DesaturatePlugIn implements JGimpPlugIn
{
	// Constants defining the names of the individual plug-ins we'll install
	private final static String COMPLETE_PLUG_IN_NAME                   = "plug_in_drawable_desaturate";
	private final static String RED_EXTRACT_PLUG_IN_NAME                = "plug_in_drawable_red_extract";
	private final static String GREEN_EXTRACT_PLUG_IN_NAME              = "plug_in_drawable_green_extract";
	private final static String BLUE_EXTRACT_PLUG_IN_NAME               = "plug_in_drawable_blue_extract";
	private final static String AVERAGE_CHANNELS_PLUG_IN_NAME           = "plug_in_drawable_channel_average";

	// Constants defining different operation modes
	private final static int    AVERAGE_CHANNELS                        = 0;
	private final static int    EXTRACT_RED                             = 1;
	private final static int    EXTRACT_GREEN                           = 2;
	private final static int    EXTRACT_BLUE                            = 3;

	// Our operation mode
	private int                 m_LastOperationMode                     = AVERAGE_CHANNELS;

	// GUI controls for our interactive dialog
	private JFrame              m_Dialog                                = null;
	private JGimpImagePanel     m_PreviewImagePanel                     = null;
	private JRadioButton        m_RedExtractRadio                       = null;
	private JRadioButton        m_BlueExtractRadio                      = null;
	private JRadioButton        m_GreenExtractRadio                     = null;
	private JRadioButton        m_DesaturateRadio                       = null;
	private JButton             m_OKButton                              = null;
	private JButton             m_CancelButton                          = null;
	private JProgressBar        m_ProgressBar                           = null;

	// Which choice the user makes in the dialog box: undefined, cancel, or one of
	// the four operation modes (AVERAGE_CHANNELS, EXTRACT_RED, EXTRACT_GREEN, EXTRACT_BLUE)
	private final static int    CHOICE_UNDEFINED                        = -2;
	private final static int    CHOICE_CANCEL                           = -1;
	private int                 m_DialogChoice                          = CHOICE_UNDEFINED;

	// The proxy server passed us when "run" was invoked. Is set to null after run is done,
	// so it also serves as a marker as to whether we are currently running
	private GimpApp          m_App                          = null;

	// Whether the current invocation is interactive
	private boolean             m_RunInteractive                        = false;

	// Variables related to our thumbnail preview
	private static final int    THUMBNAIL_SIZE                          = 100;
	private JGimpImage          m_SourceRGBThumbnailImage               = null;
	private JGimpImage          m_TargetGrayscaleThumbnailImage         = null;
	private JGimpDrawable       m_SourceRGBThumbnailDrawable            = null;
	private JGimpDrawable       m_TargetGrayscaleThumbnailDrawable      = null;


	private static final int TILE_SIZE = 512;
    public DesaturatePlugIn()
	{
		// Need default constructor with no params so we can be automatically loaded
		// We don't create the dialog box until we need it
	}

	/**
	 * Returns "true" if this plug-in is instantiated only once, with multiple
	 * calls to run() going to the same object. The alternative is that a new object
	 * is created each time "run" is called. This class always returns true (it
	 * remains resident).
	 */
	public boolean remainResident(GimpApp app)
	{
		return true;
	}

	/**
	 * Returns an array of JGimpProcedureDescriptor's that will be used
	 * to install the plug-ins in the GIMP's PDB.
	 */
    public JGimpProcedureDescriptor[] getPlugInInfo(GimpApp app)
	{
		/*
		 * Make an array of five procedure descriptors corresponding to:
		 * - a plug-in that shows the red channel only,
		 * - a plug-in that shows the green channel only,
		 * - a plug-in that shows the blue channel only, 
		 * - a plug-in that shows the average of the three channels, and
		 * - an interactive plug-in that lets the user choose between the four
		 *   different possibilities,
		 */
		JGimpProcedureDescriptor[] plugInInfoArray = new JGimpProcedureDescriptor[5];

		/*
		 * Build a list of the parameters of this plug-in
		 */
		JGimpParamDescriptorListBuilder paramBuilder = new JGimpParamDescriptorListBuilder();

		paramBuilder.append( JGimpPDBArgTypeConstants.PDB_INT32,          "run_mode", "Interactive (0), non-interactive (1)" ); // Can run interactively, or non-interactively
		paramBuilder.append( JGimpPDBArgTypeConstants.PDB_IMAGE,          "image",    "Input image"                  ); // The image we're going to work on
		paramBuilder.append( JGimpPDBArgTypeConstants.PDB_DRAWABLE,       "drawable", "Input drawable"               ); // The drawable we're going to work on

		// The base location of the menu in which we'll install all these plug-ins
		String baseMenuLocation = "Filters/Colors/Desaturation Tools";

		/*
		 * Create the complete descriptors for our 5 plug-ins
		 */
		plugInInfoArray[1] = new JGimpProcedureDescriptor(
				/* Name of the plug-in as installed in the PDB */ RED_EXTRACT_PLUG_IN_NAME,
				/* Description of the plug-in                  */ "Extracts the red channel and displays the result in a new image",
				/* The "help" message for this plug-in         */ "Extracts the red channel and displays the result in a new image",
				/* Author                                      */ "Michael Terry",
				/* Copyright information                       */ "Copyright 2002, Michael Terry",
				/* Date of writing                             */ "23 June, 2002",
				/* Menu path where it is installed             */ JGimpMenuPathConstructor.createImageMenuPath(baseMenuLocation + "/Extract red channel only"),
				/* Types of images this plug-in can work on    */ JGimpImageTypeConstructor.createAnyRGBImageType().toString(),
				/* The parameters we accept                    */ paramBuilder.toArray(),
				/* Return value descriptions (there are none)  */ null);

		plugInInfoArray[2] = new JGimpProcedureDescriptor(
				/* Name of the plug-in as installed in the PDB */ GREEN_EXTRACT_PLUG_IN_NAME,
				/* Description of the plug-in                  */ "Extracts the green channel and displays the result in a new image",
				/* The "help" message for this plug-in         */ "Extracts the green channel and displays the result in a new image",
				/* Author                                      */ "Michael Terry",
				/* Copyright information                       */ "Copyright 2002, Michael Terry",
				/* Date of writing                             */ "23 June, 2002",
				/* Menu path where it is installed             */ JGimpMenuPathConstructor.createImageMenuPath(baseMenuLocation + "/Extract green channel only"),
				/* Types of images this plug-in can work on    */ JGimpImageTypeConstructor.createAnyRGBImageType().toString(),
				/* The parameters we accept                    */ paramBuilder.toArray(),
				/* Return value descriptions (there are none)  */ null);

		plugInInfoArray[3] = new JGimpProcedureDescriptor(
				/* Name of the plug-in as installed in the PDB */ BLUE_EXTRACT_PLUG_IN_NAME,
				/* Description of the plug-in                  */ "Extracts the blue channel and displays the result in a new image",
				/* The "help" message for this plug-in         */ "Extracts the blue channel and displays the result in a new image",
				/* Author                                      */ "Michael Terry",
				/* Copyright information                       */ "Copyright 2002, Michael Terry",
				/* Date of writing                             */ "23 June, 2002",
				/* Menu path where it is installed             */ JGimpMenuPathConstructor.createImageMenuPath(baseMenuLocation + "/Extract blue channel only"),
				/* Types of images this plug-in can work on    */ JGimpImageTypeConstructor.createAnyRGBImageType().toString(),
				/* The parameters we accept                    */ paramBuilder.toArray(),
				/* Return value descriptions (there are none)  */ null);

		plugInInfoArray[4] = new JGimpProcedureDescriptor(
				/* Name of the plug-in as installed in the PDB */ AVERAGE_CHANNELS_PLUG_IN_NAME,
				/* Description of the plug-in                  */ "Averages the red, blue, and green channels and displays the result in a new image",
				/* The "help" message for this plug-in         */ "Averages the red, blue, and green channels and displays the result in a new image",
				/* Author                                      */ "Michael Terry",
				/* Copyright information                       */ "Copyright 2002, Michael Terry",
				/* Date of writing                             */ "23 June, 2002",
				/* Menu path where it is installed             */ JGimpMenuPathConstructor.createImageMenuPath(baseMenuLocation + "/Average individual channels"),
				/* Types of images this plug-in can work on    */ JGimpImageTypeConstructor.createAnyRGBImageType().toString(),
				/* The parameters we accept                    */ paramBuilder.toArray(),
				/* Return value descriptions (there are none)  */ null);

		// Add one more parameter to specify operating mode so that this function can be called programmatically
		paramBuilder.append( JGimpPDBArgTypeConstants.PDB_INT32, "operation_mode", "Average the channels (0), extract red only (1), extract green only (2), extract blue only (3)");
		plugInInfoArray[0] = new JGimpProcedureDescriptor(
				/* Name of the plug-in as installed in the PDB */ COMPLETE_PLUG_IN_NAME,
				/* Description of the plug-in                  */ "\"Desaturates\" an image by extracting either the red, green, or blue channels, or by taking the average of the three channels",
				/* The "help" message for this plug-in         */ "\"Desaturates\" an image by extracting either the red, green, or blue channels, or by taking the average of the three channels",
				/* Author                                      */ "Michael Terry",
				/* Copyright information                       */ "Copyright 2002, Michael Terry",
				/* Date of writing                             */ "23 June, 2002",
				/* Menu path where it is installed             */ JGimpMenuPathConstructor.createImageMenuPath(baseMenuLocation + "/Desaturation Dialog..."),
				/* Types of images this plug-in can work on    */ JGimpImageTypeConstructor.createAnyRGBImageType().toString(),
				/* The parameters we accept                    */ paramBuilder.toArray(),
				/* Return value descriptions (there are none)  */ null);

		// Return the array of descriptors for all of our plug-ins
		return plugInInfoArray;
	}

    /**
     * Executes the plug-in of the following name with the following data.
     */
    public synchronized JGimpPlugInReturnValues run(GimpApp app, String inName, JGimpData[] inParams) {
        try {
            // Grab our parameters
            JGimpData nonInteractive = inParams[0];
            JGimpImage theImage = (JGimpImage)inParams[1];
            JGimpDrawable theDrawable = (JGimpDrawable)inParams[2];

            // Set our run interactive flag so it can be used by other methods
            m_RunInteractive = !nonInteractive.convertToBoolean();

            // Set our current proxy
            m_App = app;

            // Check to make sure we have an RGB drawable...
            if (!theDrawable.isRGB()) {
                m_App = null;
                return JGimpPlugInReturnValues.returnExecutionError();
            }
            // For now, we'll wimp out and only consider images with 8-bits per channel
            // This is not that big of a deal right now since the GIMP only supports
            // images with up to 8-bits per channel
            int bytesPerPixel = theDrawable.getBytesPerPixel();
            if ((bytesPerPixel != 3) && (bytesPerPixel != 4)) {
                m_App = null;
                return JGimpPlugInReturnValues.returnExecutionError();
            }

            // Initialize our operation mode to the last one chosen
            int operationMode = m_LastOperationMode;

            // Set the desaturation method based on either user input or
            // how we were called as a plug-in
            if (inName.equals(COMPLETE_PLUG_IN_NAME)) {
                if (m_RunInteractive) {
                    // Get user's preference on how to "desaturate" the image
                    // After invoking getUserPreference, the dialog box stays open so we can use its
                    // progress bar to show our progress
                    operationMode = this.getUserPreference(theImage, theDrawable);

                    if (operationMode < 0) {
                        // User cancelled, so close the dialog box
                        m_Dialog.setVisible(false);
                        m_App = null;
                        return JGimpPlugInReturnValues.returnCancel();
                    }
                } else {
                    // else, running non-interactive, check for operation mode desired
                    if (inParams.length > 3) {

                        operationMode = inParams[3].convertToInt();
                        if ((operationMode < 0) || (operationMode > EXTRACT_BLUE)) {
                            return JGimpPlugInReturnValues.returnExecutionError();
                        }
                    } else {
                        ; // use our last values
                    }
                }

            } // else, user requested a specific plug-in
            else if (inName.equals(RED_EXTRACT_PLUG_IN_NAME)) {
                operationMode = EXTRACT_RED;
            } else if (inName.equals(GREEN_EXTRACT_PLUG_IN_NAME)) {
                operationMode = EXTRACT_GREEN;
            } else if (inName.equals(BLUE_EXTRACT_PLUG_IN_NAME)) {
                operationMode = EXTRACT_BLUE;
            } else {
                operationMode = AVERAGE_CHANNELS;
            }
            // Now operationMode has been set to the desired mode. Let's go to work!

            // Get the size of the drawable
            Dimension drawableSize = theDrawable.getSize();

            // Make a new grayscale image and add a new layer
            JGimpImage newImage = app.createGrayscaleImage(drawableSize.width, drawableSize.height);
            JGimpLayer newLayer =
                newImage.appendNewLayer(drawableSize.width, drawableSize.height, false, "Untitled-1", 100, 0);

            // Perform the actual desaturation
            switch (operationMode) {
                case EXTRACT_RED :
                    extractRed(theDrawable, newLayer);
                    break;

                case EXTRACT_BLUE :
                    extractBlue(theDrawable, newLayer);
                    break;

                case EXTRACT_GREEN :
                    extractGreen(theDrawable, newLayer);
                    break;

                default :
                    averageChannels(theDrawable, newLayer);
            }

            // Display the new image
            newImage.display();

            // Flush the displays
            app.flushDisplays();

            // Hide our dialog box if we are running interactively
            if (m_Dialog != null) {
                m_Dialog.setVisible(false);
            }

            // Clear the proxy variable to indicate that we are no longer in "run"
            m_App = null;

            // Save the operation mode we used
            m_LastOperationMode = operationMode;

        } catch (Exception e) {

            // Caught an exception somewhere along the line...
            // Close up our stuff, and show an error dialog
            m_App = null;
            if (m_Dialog != null) {
                m_Dialog.setVisible(false);
            }

            JOptionPane.showMessageDialog(
                null,
                "Error invoking desaturate plug-in",
                "Error",
                JOptionPane.ERROR_MESSAGE);

            // Print out some debugging info
            System.err.println("Error caught in DesaturatePlugIn: " + e.getMessage());
            e.printStackTrace();
            return JGimpPlugInReturnValues.returnExecutionError();
        }

        // Return error
        return JGimpPlugInReturnValues.returnSuccess();
    }

    private void averageChannels(JGimpDrawable inSourceRGBDrawable, JGimpDrawable inTargetGrayscaleDrawable)
        throws
            JGimpProxyException,
            JGimpInvalidDrawableException,
            JGimpInvalidPixelRegionException,
            JGimpBufferOverflowException,
            JGimpException {

        Dimension drawableSize = inSourceRGBDrawable.getSize();

        int[] pixelData = null;
        byte[] newPixels = null;
        if (m_RunInteractive && (m_DialogChoice >= 0)) {
            m_ProgressBar.setMaximum(drawableSize.width * drawableSize.height / TILE_SIZE + 1);
            m_ProgressBar.setValue(0);
        }
        for (int y_offset = 0; y_offset < drawableSize.height; y_offset += TILE_SIZE) {
            for (int x_offset = 0; x_offset < drawableSize.width; x_offset += TILE_SIZE) {
                int this_width =
                    ((drawableSize.width - x_offset) < TILE_SIZE) ? (drawableSize.width - x_offset) : TILE_SIZE;
                int this_height =
                    ((drawableSize.height - y_offset) < TILE_SIZE) ? (drawableSize.height - y_offset) : TILE_SIZE;
                if (pixelData == null) {
                    pixelData =
                        inSourceRGBDrawable.readPixelRegionInJavaIntFormat(x_offset, y_offset, this_width, this_height);
                } else {
                    inSourceRGBDrawable.readPixelRegionInJavaIntFormat(
                        x_offset,
                        y_offset,
                        this_width,
                        this_height,
                        pixelData,
                        0);
                }

                // Iterate through each pixel, averaging it, and writing it to the new array
                int numPixels = this_width * this_height;

                if ((newPixels == null) || (newPixels.length < numPixels)) {
                    newPixels = new byte[numPixels];
                }
                for (int i = 0; i < numPixels; i++) {
                    int redChannel = (pixelData[i] >> 16) & 0xFF;
                    int blueChannel = (pixelData[i] >> 8) & 0xFF;
                    int greenChannel = pixelData[i] & 0xFF;
                    int average = (redChannel + blueChannel + greenChannel) / 3;

                    newPixels[i] = (byte)average;
                }
                // Write the pixels back to the new image
                inTargetGrayscaleDrawable.writePixelRegionInNativeByteFormat(
                    x_offset,
                    y_offset,
                    this_width,
                    this_height,
                    newPixels,
                    0);
                // Increment our progress bar
                if (m_RunInteractive && (m_DialogChoice >= 0)) {
                    m_ProgressBar.setValue((y_offset * drawableSize.width + x_offset) / TILE_SIZE + 1);
                }
            }
        }
    }

    private void extractRed(JGimpDrawable inSourceRGBDrawable, JGimpDrawable inTargetGrayscaleDrawable)
        throws
            JGimpProxyException,
            JGimpInvalidDrawableException,
            JGimpInvalidPixelRegionException,
            JGimpBufferOverflowException,
            JGimpException {
        extractChannel(inSourceRGBDrawable, inTargetGrayscaleDrawable, 16);
    }

    private void extractGreen(JGimpDrawable inSourceRGBDrawable, JGimpDrawable inTargetGrayscaleDrawable)
        throws
            JGimpProxyException,
            JGimpInvalidDrawableException,
            JGimpInvalidPixelRegionException,
            JGimpBufferOverflowException,
            JGimpException {
        extractChannel(inSourceRGBDrawable, inTargetGrayscaleDrawable, 8);
    }

    private void extractBlue(JGimpDrawable inSourceRGBDrawable, JGimpDrawable inTargetGrayscaleDrawable)
        throws
            JGimpProxyException,
            JGimpInvalidDrawableException,
            JGimpInvalidPixelRegionException,
            JGimpBufferOverflowException,
            JGimpException {
        extractChannel(inSourceRGBDrawable, inTargetGrayscaleDrawable, 0);
    }

    private void extractChannel(
        JGimpDrawable inSourceRGBDrawable,
        JGimpDrawable inTargetGrayscaleDrawable,
        int inBitShift)
        throws
            JGimpProxyException,
            JGimpInvalidDrawableException,
            JGimpInvalidPixelRegionException,
            JGimpBufferOverflowException,
            JGimpException {

        Dimension drawableSize = inSourceRGBDrawable.getSize();
        int[] pixelData = null;
        byte[] newPixels = null;
        if (m_RunInteractive && (m_DialogChoice >= 0)) {
            m_ProgressBar.setMaximum(drawableSize.width * drawableSize.height / TILE_SIZE + 1);
            m_ProgressBar.setValue(0);
        }
        for (int y_offset = 0; y_offset < drawableSize.height; y_offset += TILE_SIZE) {
            for (int x_offset = 0; x_offset < drawableSize.width; x_offset += TILE_SIZE) {
                int this_width =
                    ((drawableSize.width - x_offset) < TILE_SIZE) ? (drawableSize.width - x_offset) : TILE_SIZE;
                int this_height =
                    ((drawableSize.height - y_offset) < TILE_SIZE) ? (drawableSize.height - y_offset) : TILE_SIZE;
                if (pixelData == null) {
                    pixelData =
                        inSourceRGBDrawable.readPixelRegionInJavaIntFormat(x_offset, y_offset, this_width, this_height);
                } else {
                    inSourceRGBDrawable.readPixelRegionInJavaIntFormat(
                        x_offset,
                        y_offset,
                        this_width,
                        this_height,
                        pixelData,
                        0);
                }

                // Iterate through each pixel, extracting it, and writing it to the new array
                int numPixels = this_width * this_height;

                if ((newPixels == null) || (newPixels.length < numPixels)) {
                    newPixels = new byte[numPixels];
                }
                for (int i = 0; i < numPixels; i++) {
                    newPixels[i] = (byte) ((pixelData[i] >> inBitShift) & 0xFF);

                }
                // Write the pixels back to the new image
                inTargetGrayscaleDrawable.writePixelRegionInNativeByteFormat(
                    x_offset,
                    y_offset,
                    this_width,
                    this_height,
                    newPixels,
                    0);
                // Increment our progress bar
                if (m_RunInteractive && (m_DialogChoice >= 0)) {
                    m_ProgressBar.setValue((y_offset * drawableSize.width + x_offset) / TILE_SIZE + 1);
                }
            }
        }
    }

    /**
     * Called if the plug-in remains resident and the application is quitting
     */
    public void stop(GimpApp app) {
        if (m_Dialog == null) {
            // never initialized
            return;
        }
        // We might not get here if "run" is currently invoked because the
        // GIMP might now call us since we are "running". In any case,
        // the following code would shut us down properly

        // Close our dialog if it is currently open
        if (m_Dialog.isVisible()) {
            m_DialogChoice = CHOICE_CANCEL;
            m_Dialog.setVisible(false);
        }
        // Wait for the "run" method to exit
        // When m_App has been set to "null", then we know it has exited
        // We'll only try about 10 times, then just give up
        for (int i = 0; i < 10; i++) {
            if (m_App == null) {
                return;
            }
            try {
                Thread.sleep(100);
            } catch (Exception e) {
            }
        }
    }

    /**
     * Displays a dialog box with preview and progress bar to prompt the
     * user for which desaturation method they prefer. The dialog box
     * remains open after this method invocation, so that the other methods
     * can use its progress bar to update their progress
     */
    private int getUserPreference(JGimpImage inSourceImage, JGimpDrawable inSourceDrawable) {
        createDialog();

        try {
            m_SourceRGBThumbnailImage = JGimpMacros.createThumbnail(inSourceImage, THUMBNAIL_SIZE, THUMBNAIL_SIZE);
            m_SourceRGBThumbnailDrawable = m_SourceRGBThumbnailImage.getActiveDrawable();
            Dimension thumbnailSize = m_SourceRGBThumbnailDrawable.getSize();
            m_TargetGrayscaleThumbnailImage = m_App.createGrayscaleImage(thumbnailSize.width, thumbnailSize.height);
            m_TargetGrayscaleThumbnailDrawable =
                m_TargetGrayscaleThumbnailImage.appendNewLayer(
                    thumbnailSize.width,
                    thumbnailSize.height,
                    false,
                    "",
                    100,
                    0);
        } catch (Exception e) {

            JOptionPane.showMessageDialog(
                null,
                "Error invoking desaturate plug-in. Could not make previews",
                "Error",
                JOptionPane.ERROR_MESSAGE);
            return CHOICE_CANCEL;
        }

        // First set the radio to the last preference
        // We use "doClick" so an ActionEvent is fired, causing the preview to update
        switch (m_LastOperationMode) {
            case EXTRACT_RED :
                m_RedExtractRadio.doClick();
                break;

            case EXTRACT_GREEN :
                m_GreenExtractRadio.doClick();
                break;

            case EXTRACT_BLUE :
                m_BlueExtractRadio.doClick();
                break;

            default :
                m_DesaturateRadio.doClick();
        }

        // Make the dialog visible, wait for a choice to be made
        m_DialogChoice = CHOICE_UNDEFINED;
        m_Dialog.setVisible(true);
        m_Dialog.requestFocus();
        m_ProgressBar.setValue(0);
        while (m_DialogChoice == CHOICE_UNDEFINED) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {
            }
        }

        // Clean up any thumbnails generated
        if (m_SourceRGBThumbnailImage != null) {
            try {
                m_SourceRGBThumbnailImage.delete();
            } catch (Exception e) {
            }
            m_SourceRGBThumbnailImage = null;
            m_SourceRGBThumbnailDrawable = null;
        }
        if (m_TargetGrayscaleThumbnailImage != null) {
            try {
                m_TargetGrayscaleThumbnailImage.delete();
            } catch (Exception e) {
            }
            m_TargetGrayscaleThumbnailImage = null;
            m_TargetGrayscaleThumbnailDrawable = null;
        }

        return m_DialogChoice;
    }

    private void createDialog() {
        // Your standard Java GUI stuff, so I won't bother to comment it

        if (m_Dialog != null) {
            return;
        }
        m_Dialog = new JFrame("Desaturation");
        m_Dialog.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
        m_Dialog.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                m_DialogChoice = CHOICE_CANCEL;
            }
        });

        m_PreviewImagePanel = new JGimpImagePanel();
        JPanel previewPanel = new JPanel();
        m_PreviewImagePanel.setBackground(previewPanel.getBackground());
        // JGimpImagePanel has a white background by default
        m_PreviewImagePanel.setSize(THUMBNAIL_SIZE, THUMBNAIL_SIZE);
        m_PreviewImagePanel.setPreferredSize(m_PreviewImagePanel.getSize());
        m_PreviewImagePanel.setMaximumSize(m_PreviewImagePanel.getSize());
        previewPanel.add(m_PreviewImagePanel);
        previewPanel.setSize(THUMBNAIL_SIZE, THUMBNAIL_SIZE);
        previewPanel.setPreferredSize(previewPanel.getSize());
        previewPanel.setMaximumSize(previewPanel.getSize());

        m_RedExtractRadio = new JRadioButton("Extract red channel");
        m_GreenExtractRadio = new JRadioButton("Extract green channel");
        m_BlueExtractRadio = new JRadioButton("Extract blue channel");
        m_DesaturateRadio = new JRadioButton("Desaturate whole image");

        m_RedExtractRadio.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent inEvent) {
                if ((m_SourceRGBThumbnailDrawable != null) && (m_TargetGrayscaleThumbnailDrawable != null)) {
                    try {
                        extractRed(m_SourceRGBThumbnailDrawable, m_TargetGrayscaleThumbnailDrawable);
                        m_PreviewImagePanel.copyDrawable(m_TargetGrayscaleThumbnailDrawable);
                    } catch (Exception e) {
                        m_PreviewImagePanel.clearImage();
                    }
                }
            }
        });
        m_GreenExtractRadio.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent inEvent) {
                if ((m_SourceRGBThumbnailDrawable != null) && (m_TargetGrayscaleThumbnailDrawable != null)) {
                    try {
                        extractGreen(m_SourceRGBThumbnailDrawable, m_TargetGrayscaleThumbnailDrawable);
                        m_PreviewImagePanel.copyDrawable(m_TargetGrayscaleThumbnailDrawable);
                    } catch (Exception e) {
                        m_PreviewImagePanel.clearImage();
                    }
                }
            }
        });
        m_BlueExtractRadio.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent inEvent) {
                if ((m_SourceRGBThumbnailDrawable != null) && (m_TargetGrayscaleThumbnailDrawable != null)) {
                    try {
                        extractBlue(m_SourceRGBThumbnailDrawable, m_TargetGrayscaleThumbnailDrawable);
                        m_PreviewImagePanel.copyDrawable(m_TargetGrayscaleThumbnailDrawable);
                    } catch (Exception e) {
                        m_PreviewImagePanel.clearImage();
                    }
                }
            }
        });
        m_DesaturateRadio.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent inEvent) {
                if ((m_SourceRGBThumbnailDrawable != null) && (m_TargetGrayscaleThumbnailDrawable != null)) {
                    try {
                        averageChannels(m_SourceRGBThumbnailDrawable, m_TargetGrayscaleThumbnailDrawable);
                        m_PreviewImagePanel.copyDrawable(m_TargetGrayscaleThumbnailDrawable);
                    } catch (Exception e) {
                        m_PreviewImagePanel.clearImage();
                    }
                }
            }
        });

        JPanel radioPanel = new JPanel();
        radioPanel.setLayout(new GridLayout(0, 1));
        radioPanel.add(m_RedExtractRadio);
        radioPanel.add(m_GreenExtractRadio);
        radioPanel.add(m_BlueExtractRadio);
        radioPanel.add(m_DesaturateRadio);

        ButtonGroup buttonGroup = new ButtonGroup();
        buttonGroup.add(m_RedExtractRadio);
        buttonGroup.add(m_GreenExtractRadio);
        buttonGroup.add(m_BlueExtractRadio);
        buttonGroup.add(m_DesaturateRadio);

        m_OKButton = new JButton("OK");
        m_CancelButton = new JButton("Cancel");

        m_OKButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (m_RedExtractRadio.isSelected()) {
                    m_DialogChoice = EXTRACT_RED;
                } else if (m_GreenExtractRadio.isSelected()) {
                    m_DialogChoice = EXTRACT_GREEN;
                } else if (m_BlueExtractRadio.isSelected()) {
                    m_DialogChoice = EXTRACT_BLUE;
                } else {
                    m_DialogChoice = AVERAGE_CHANNELS;
                }
            }
        });
        m_CancelButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                m_DialogChoice = CHOICE_CANCEL;
            }
        });
        JPanel okCancelPanel = new JPanel();
        okCancelPanel.setLayout(new GridLayout(1, 2));
        okCancelPanel.add(m_CancelButton);
        okCancelPanel.add(m_OKButton);

        m_ProgressBar = new JProgressBar();
        m_ProgressBar.setMinimum(0);

        JPanel fullPanel = new JPanel();
        fullPanel.setLayout(new BoxLayout(fullPanel, BoxLayout.Y_AXIS));
        fullPanel.add(previewPanel);
        fullPanel.add(radioPanel);
        fullPanel.add(okCancelPanel);
        fullPanel.add(m_ProgressBar);

        m_Dialog.getContentPane().add(fullPanel);
        m_Dialog.pack();
    }
}
