| PlugInFilterRunner |

    package ij.plugin.filter;
    import ij.*;
    import ij.process.*;
    import ij.gui.*;
    import ij.plugin.filter.PlugInFilter.*;
    import ij.plugin.filter.*;
    import ij.measure.Calibration;
    import ij.macro.Interpreter;
    import java.awt.*;
    import java.util.Hashtable;

    public class PlugInFilterRunner implements Runnable, DialogListener {
        private String command;                     // the command, can be but need not be the name of the PlugInFilter
        private Object theFilter;                   // the instance of the PlugInFilter
        private ImagePlus imp;
        private int flags;                          // the flags returned by the PlugInFilter
        private boolean snapshotDone;       // whether the ImageProcessor has a snapshot already
        private boolean previewCheckboxOn;          // the state of the preview checkbox (true = on)
        private boolean bgPreviewOn;        // tells the background thread that preview is allowed
        private boolean bgKeepPreview;      // tells the background thread to keep the result of preview
        private Thread previewThread;        // the thread of the preview
        private GenericDialog gd;            // the dialog (if it registers here by setDialog)
        private Checkbox previewCheckbox;    // reference to the preview Checkbox of the dialog
        private long previewTime;               // time (ms) needed for preview processing
        private boolean ipChanged;          // whether the image data have been changed
        private int processedAsPreview;         // the slice processed during preview (if non-zero)
        private Hashtable slicesForThread;          // gives first&last slice that a given thread should process
        Hashtable sliceForThread = new Hashtable(); // here the stack slice currently processed is stored.
        private int nPasses;                        // the number of calls to the run(ip) method of the filter
        private int pass;                       // passes done so far
        private boolean doStack;

        /** The constructor runs a PlugInFilter or ExtendedPlugInFilter by calling its
         * setup, run, etc. methods. For details, see the documentation of interfaces
         * PlugInFilter and ExtendedPlugInFilter.
         * @param theFilter The PlugInFilter to be run
         * @param command   The command that has caused running the PlugInFilter
         * @param arg       The argument specified for this PlugInFilter in IJ_Props.txt or in the
         * plugins.config file of a .jar archive conatining a collection of plugins. <code>arg</code>
         * may be a string of length zero.
         */
        public PlugInFilterRunner(Object theFilter, String command, String arg) {
            this.theFilter = theFilter;
            this.command = command;
            imp = WindowManager.getCurrentImage();
            flags = ((PlugInFilter)theFilter).setup(arg, imp);  // S E T U P
            if ((flags&PlugInFilter.DONE)!=0) return;
            if (!checkImagePlus(imp, flags, command)) return;   // check whether the PlugInFilter can handle this image type
            if ((flags&PlugInFilter.NO_IMAGE_REQUIRED)!=0)
                imp = null;                                 // if the plugin does not want an image, it should not get one
            Roi roi = null;
            if (imp != null) {
                roi = imp.getRoi();
                if (roi!=null) roi.endPaste();              // prepare the image: finish previous paste operation (if any)
                if (!imp.lock()) return;                    // exit if image is in use
                nPasses = imp.getProcessor().getNChannels();
            }
            if (theFilter instanceof ExtendedPlugInFilter) { // calling showDialog required?
                flags = ((ExtendedPlugInFilter)theFilter).showDialog(imp, command, this);  // D I A L O G (may include preview)
                if (snapshotDone)
                    Undo.setup(Undo.FILTER, imp);           // ip has a snapshot that may be used for Undo
                boolean keepPreviewFlag = (flags&ExtendedPlugInFilter.KEEP_PREVIEW)!=0;
                if (keepPreviewFlag && imp!=null && previewThread!=null && ipChanged &&
                        previewCheckbox!=null && previewCheckboxOn) {
                    bgKeepPreview = true;
                    waitForPreviewDone();
                    processedAsPreview = imp.getCurrentSlice();
                } else {
                    killPreview();
                    previewTime = 0;
                }
            } // if ExtendedPlugInFilter
            if ((flags&PlugInFilter.DONE)!=0) {
                if (imp != null) imp.unlock();
                return;
            } else if (imp==null) {
                ((PlugInFilter)theFilter).run(null);        // not DONE, but NO_IMAGE_REQUIRED
                return;
            }
            /* preparing for the run(ip) method of the PlugInFilter... */
            int slices = imp.getStackSize();
            //IJ.log("processedAsPreview="+processedAsPreview+"; slices="+slices+"; doesStacks="+((flags&PlugInFilter.DOES_STACKS)!=0));
            doStack = slices>1 && (flags&PlugInFilter.DOES_STACKS)!=0;
            imp.startTiming();
            if (doStack || processedAsPreview==0) {             // if processing during preview was not enough
                IJ.showStatus(command + (doStack ? " (Stack)..." : "..."));
                ImageProcessor ip = imp.getProcessor();
                pass = 0;
                if (!doStack) {                                 // S I N G L E   I M A G E
                    FloatProcessor fp = null;
                    prepareProcessor(ip, imp);
                    announceSliceNumber(imp.getCurrentSlice());
                    if (theFilter instanceof ExtendedPlugInFilter)
                        ((ExtendedPlugInFilter)theFilter).setNPasses(nPasses);
                    if ((flags&PlugInFilter.NO_CHANGES)==0) {   // for filters modifying the image
                        boolean disableUndo = Prefs.disableUndo || (flags&PlugInFilter.NO_UNDO)!=0;
                        if (!disableUndo) {
                            ip.snapshot();
                            snapshotDone = true;
                        }
                    }
                    processOneImage(ip, fp, snapshotDone);      // may also set snapShotDone
                    if ((flags&PlugInFilter.NO_CHANGES)==0) {   // (filters doing no modifications don't change undo status)
                        if (snapshotDone)
                            Undo.setup(Undo.FILTER, imp);
                        else
                            Undo.reset();
                    }
                    if ((flags&PlugInFilter.NO_CHANGES)==0) ip.resetBinaryThreshold();
                } else {  //  S T A C K
                    Undo.reset();    // no undo for processing a complete stack
                    IJ.resetEscape();
                    int slicesToDo = processedAsPreview!=0 ? slices-1 : slices;
                    nPasses *= slicesToDo;
                    if (theFilter instanceof ExtendedPlugInFilter)
                        ((ExtendedPlugInFilter)theFilter).setNPasses(nPasses);
                    int threads = 1;
                    if ((flags&PlugInFilter.PARALLELIZE_STACKS)!=0) {
                        threads = Prefs.getThreads(); // multithread support for multiprocessor machines
                        if (threads>slicesToDo) threads = slicesToDo;
                        if (threads>1) slicesForThread = new Hashtable(threads-1);
                    }
                    int startSlice = 1;
                    for (int i=1; i<threads; i++) {             // setup the background threads
                        int endSlice = (slicesToDo*i)/threads;
                        if (processedAsPreview!=0 && processedAsPreview<=endSlice) endSlice++;
                        Thread bgThread = new Thread(this, command+" "+startSlice+"-"+endSlice);
                        slicesForThread.put(bgThread, new int[] {startSlice, endSlice});
                        bgThread.start();
                        //IJ.log("Stack: Thread for slices "+startSlice+"-"+endSlice+" started");
                        startSlice = endSlice+1;
                    }
                    //IJ.log("Stack: Slices "+startSlice+"-"+slices+" by main thread");
                    processStack(startSlice, slices);           // the current thread does the rest
                    if (slicesForThread != null) {
                        while (slicesForThread.size()>0) {    // for all other threads:
                            Thread theThread = (Thread)slicesForThread.keys().nextElement();
                            try {
                                theThread.join();               // wait until thread has finished
                            } catch (InterruptedException e) {}
                            slicesForThread.remove(theThread);  // and remove it from the list.
                        }
                    }
                }
            } // end processing:
            if ((flags&PlugInFilter.FINAL_PROCESSING)!=0 && !IJ.escapePressed())
                ((PlugInFilter)theFilter).setup("final", imp);
            if (IJ.escapePressed())
                IJ.showStatus(command + " INTERRUPTED");
            else
                IJ.showTime(imp, imp.getStartTime()-previewTime, command + ": ", doStack?slices:1);
            IJ.showProgress(1.0);
            if (ipChanged) {
                imp.changes = true;
                imp.updateAndDraw();
            }
            ImageWindow win = imp.getWindow();
            if (win!=null) {
                win.running = false;
                win.running2 = false;
            }
            imp.unlock();
        }

        /** Process a stack or part of it. The slice given by class variable
         *  processedAsPreview remains unchanged.
         * @param firstSlice Slice number of the first slice to be processed
         * @param endSlice   Slice number of the last slice to be processed
         */
        private void processStack(int firstSlice, int endSlice) {
            ImageStack stack = imp.getStack();
            ImageProcessor ip = stack.getProcessor(firstSlice);
            prepareProcessor(ip, imp);
            ip.setLineWidth(Line.getWidth());       //in contrast to imp.getProcessor, stack.getProcessor does not do this
            FloatProcessor fp = null;
            int slices = imp.getNSlices();
            for (int i=firstSlice; i<=endSlice; i++) {
                if (i != processedAsPreview) {
                    announceSliceNumber(i);
                    ip.setPixels(stack.getPixels(i));
                    processOneImage(ip, fp, false);
                    if (IJ.escapePressed()) {IJ.beep(); break;}
                }
            }
        }

        /** prepare an ImageProcessor by setting roi and CalibrationTable.
         */
        private void prepareProcessor(ImageProcessor ip, ImagePlus imp) {
            ImageProcessor mask = imp.getMask();
            Roi roi = imp.getRoi();
            if (roi!=null && roi.isArea())
                ip.setRoi(roi);
            else
                ip.setRoi((Roi)null);
            if (imp.getNSlices()>1) {
                ImageProcessor ip2 = imp.getProcessor();
                double minThreshold = ip.getMinThreshold();
                double maxThreshold = ip.getMaxThreshold();
                if (minThreshold!=ImageProcessor.NO_THRESHOLD)
                    ip.setThreshold(minThreshold, maxThreshold, ImageProcessor.NO_LUT_UPDATE);
            }
            float[] cTable = imp.getCalibration().getCTable();
            ip.setCalibrationTable(cTable);
        }

        /**
         * Process a single image with the PlugInFilter.
         * @param ip        The image data that should be processed
         * @param fp        A Floatprocessor as a target for conversion to Float. May be null.
         * @param snapshotDone Whether a snapshot of ip has been be taken previously. if
         * snapshotDone is true, the snapshot needed for the SUPPORTS_MASKING or SNAPSHOT
         * flags of the filter is not created any more.
         * Class variables used: flags (input), snapshotDone (set if a snapshot of ip
         * is taken), ipChanged (set if ip was probably changed).
         */
        private void processOneImage(ImageProcessor ip, FloatProcessor fp, boolean snapshotDone) {
            Thread thread = Thread.currentThread();
            boolean convertToFloat = (flags&PlugInFilter.CONVERT_TO_FLOAT)!=0 && !(ip instanceof FloatProcessor);
            boolean doMasking = (flags&PlugInFilter.SUPPORTS_MASKING)!=0 && ip.getMask() != null;
            if (!snapshotDone && (doMasking || ((flags&PlugInFilter.SNAPSHOT)!=0) && !convertToFloat)) {
                ip.snapshot();
                this.snapshotDone = true;
            }
            if (convertToFloat) {
                for (int i=0; i<ip.getNChannels(); i++) {
                    fp = ip.toFloat(i, fp);
                    if (thread.isInterrupted()) return;         // interrupt processing for preview?
                    if ((flags&PlugInFilter.SNAPSHOT)!=0) fp.snapshot();
                    if (doStack) IJ.showProgress(pass/(double)nPasses);
                    ((PlugInFilter)theFilter).run(fp);
                    if (thread.isInterrupted()) return;
                    //IJ.log("slice="+getSliceNumber()+" pass="+pass+"/"+nPasses);
                    pass++;
                    if ((flags&PlugInFilter.NO_CHANGES)==0) {
                        ipChanged = true;
                        ip.setPixels(i, fp);
                    }
                }
            } else {
                if ((flags&PlugInFilter.NO_CHANGES)==0) ipChanged = true;
                    if (doStack) IJ.showProgress(pass/(double)nPasses);
                ((PlugInFilter)theFilter).run(ip);
                pass++;
            }
            if (thread.isInterrupted()) return;
            if (doMasking)
                ip.reset(ip.getMask());  //restore image outside irregular roi
       }

        /** test whether an ImagePlus can be processed based on the flags specified
         *  and display an error message if not.
         */
        private boolean checkImagePlus(ImagePlus imp, int flags, String cmd) {
            boolean imageRequired = (flags&PlugInFilter.NO_IMAGE_REQUIRED)==0;
            if (imageRequired && imp==null)
                {IJ.noImage(); return false;}
            if (imageRequired) {
                if (imp.getProcessor()==null)
                    {wrongType(flags, cmd); return false;}
                int type = imp.getType();
                switch (type) {
                    case ImagePlus.GRAY8:
                        if ((flags&PlugInFilter.DOES_8G)==0)
                        {wrongType(flags, cmd); return false;}
                        break;
                    case ImagePlus.COLOR_256:
                        if ((flags&PlugInFilter.DOES_8C)==0)
                        {wrongType(flags, cmd); return false;}
                        break;
                    case ImagePlus.GRAY16:
                        if ((flags&PlugInFilter.DOES_16)==0)
                        {wrongType(flags, cmd); return false;}
                        break;
                    case ImagePlus.GRAY32:
                        if ((flags&PlugInFilter.DOES_32)==0)
                        {wrongType(flags, cmd); return false;}
                        break;
                    case ImagePlus.COLOR_RGB:
                        if ((flags&PlugInFilter.DOES_RGB)==0)
                        {wrongType(flags, cmd); return false;}
                        break;
                }
                if ((flags&PlugInFilter.ROI_REQUIRED)!=0 && imp.getRoi()==null)
                {IJ.error(cmd+": Selection required"); return false;}
                if ((flags&PlugInFilter.STACK_REQUIRED)!=0 && imp.getStackSize()==1)
                {IJ.error(cmd+": Stack required"); ; return false;}
            } // if imageRequired
            return true;
        }

        /** Display an error message, telling the allowed image types
         */
        static void wrongType(int flags, String cmd) {
            String s = "\""+cmd+"\" requires an image of type:\n \n";
            if ((flags&PlugInFilter.DOES_8G)!=0) s +=  "    8-bit grayscale\n";
            if ((flags&PlugInFilter.DOES_8C)!=0) s +=  "    8-bit color\n";
            if ((flags&PlugInFilter.DOES_16)!=0) s +=  "    16-bit grayscale\n";
            if ((flags&PlugInFilter.DOES_32)!=0) s +=  "    32-bit (float) grayscale\n";
            if ((flags&PlugInFilter.DOES_RGB)!=0) s += "    RGB color\n";
            IJ.error(s);
        }

        /** Make the slice number accessible to the PlugInFilter by putting it
         * into the appropriate hashtable.
         */
        private void announceSliceNumber(int slice) {
            synchronized(sliceForThread){
                Integer number = new Integer(slice);
                sliceForThread.put(Thread.currentThread(), number);
            }
        }

        /** Return the slice number currently processed by the calling thread.
         * @return The slice number. Returns -1 on error (when not processing).
         */
        public int getSliceNumber() {
            synchronized(sliceForThread){
                Integer number = (Integer)sliceForThread.get(Thread.currentThread());
                return (number == null) ? -1 : number.intValue();
            }
        }

        /** The dispatcher for the background threads
         */
        public void run() {
            Thread thread = Thread.currentThread();
            try {
                if (thread == previewThread)
                    runPreview();
                else if (slicesForThread!=null && slicesForThread.containsKey(thread)) {
                    int[] range = (int[])slicesForThread.get(thread);
                    processStack(range[0], range[1]);
                } else
                    IJ.error("PlugInFilterRunner internal error:\nunsolicited background thread");
            } catch (Exception err) {
                IJ.beep();
                IJ.log("ERROR: "+err+"\nin "+thread.getName()+
                "\nat "+(err.getStackTrace()[0])+"\nfrom "+(err.getStackTrace()[1]));  //requires Java 1.4
            }
        }

        /** The background thread for preview
         */
        private void runPreview() {
            if (IJ.debugMode) IJ.log("preview thread started; imp="+imp.getTitle());
            Thread thread = Thread.currentThread();
            ImageProcessor ip = imp.getProcessor();
            Roi originalRoi = imp.getRoi();
            FloatProcessor fp = null;
            prepareProcessor(ip, imp);
            announceSliceNumber(imp.getCurrentSlice());
            if (!snapshotDone && (flags&PlugInFilter.NO_CHANGES)==0) {
                ip.snapshot();
                snapshotDone = true;
            }
            boolean previewDataOk = false;
            while(bgPreviewOn) {
                if (previewCheckboxOn) gd.previewRunning(true);// optical feedback
                interruptable: {
                    if (imp.getRoi() != originalRoi) {
                        imp.setRoi(originalRoi);        // restore roi; the PlugInFilter may have affected it
                        if (originalRoi!=null && originalRoi.isArea())
                            ip.setRoi(originalRoi);
                        else
                            ip.setRoi((Roi)null);
                    }
                    if (ipChanged)                      // restore image data if necessary
                        ip.reset();
                    ipChanged = false;
                    previewDataOk = false;
                    long startTime = System.currentTimeMillis();
                    pass = 0;
                    if (theFilter instanceof ExtendedPlugInFilter)
                        ((ExtendedPlugInFilter)theFilter).setNPasses(nPasses); //this should reset pass in the filter
                    if (thread.isInterrupted())
                        break interruptable;
                    processOneImage(ip, fp, true);      // P R O C E S S   (sets ipChanged)
                    IJ.showProgress(1.0);
                    if (thread.isInterrupted())
                        break interruptable;
                    previewDataOk = true;
                    previewTime = System.currentTimeMillis() - startTime;
                    imp.updateAndDraw();
                    if (IJ.debugMode) IJ.log("preview processing done");
                }
                gd.previewRunning(false);               // optical feedback
                IJ.showStatus("");                      //delete last status messages from processing
                synchronized(this) {
                    if (!bgPreviewOn)
                        break;                          //thread should stop and possibly keep the data
                    try {
                        wait();                         //wait for interrupted (don't keep preview) or notify (keep preview)
                    } catch (InterruptedException e) {
                        previewDataOk = false;
                    }
                } // synchronized
            } // while bgPreviewOn
            if (thread.isInterrupted())
                previewDataOk = false;                  //interrupted always means "don't keep preview"
            if (!previewDataOk || !bgKeepPreview) {     //no need to keep the result
                imp.setRoi(originalRoi);                //restore roi
                if (ipChanged) {                        //revert the image data
                    ip.reset();
                    ipChanged = false;
                }
            }
            imp.updateAndDraw();                        //display current state of image (reset or result of preview)
            sliceForThread.remove(thread);              //no need to announce the slice number any more
         }

        /** stop the background process responsible for preview as fast as possible
         and wait until the preview thread has finished */
        private void killPreview() {
            if (previewThread == null) return;
            //IJ.log("killPreview");
            synchronized (this) {
                previewThread.interrupt();              //ask for premature finishing (interrupt first -> no keepPreview)
                bgPreviewOn = false;                    //tell a possible background thread to terminate when it has finished
            }
            waitForPreviewDone();
        }

        /** stop the background process responsible for preview and wait until the preview thread has finished */
        private void waitForPreviewDone() {
            if (previewThread.isAlive()) try {      //a NullPointerException is possible if the thread finishes in the meanwhile
                previewThread.setPriority(Thread.currentThread().getPriority());
            } catch (Exception e) {}
            synchronized (this) {
                bgPreviewOn = false;                //tell a possible background thread to terminate
                notify();                           //(but finish processing unless interrupted)
            }
            try {previewThread.join();}             //wait until the background thread is done
            catch (InterruptedException e){}
            previewThread = null;
        }

        /* set the GenericDialog gd where the preview comes from (if gd is
         * suitable for listening, i.e., if it has a preview checkbox).
         */
        public void setDialog(GenericDialog gd) {
            if (gd != null && imp != null) {
                previewCheckbox = gd.getPreviewCheckbox();
                if (previewCheckbox != null) {
                    gd.addDialogListener(this);
                    this.gd = gd;
                }
            } //IJ.log("setDialog done");
        }

        /** The listener to any change in the dialog. It is used for preview.
         * It is invoked every time the user changes something in the dialog
         * (except OK and cancel buttons), provided that all previous
         * listeners (parameter checking) have returned true.
         *
         * @param e The event that has happened in the dialog. This method may
         *          be also called with e=null, e.g. to start preview already
         *          when the dialog appears.
         * @return  Always true. (The return value determines whether the
         *          dialog will enable the OK button)
         */
        public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) {
            //IJ.log("PlugInFilterRunner: dialogItemChanged: "+e);
            if (previewCheckbox == null || imp == null) return true;
            previewCheckboxOn = previewCheckbox.getState();
            //IJ.log("PlugInFilterRunner in DialogItemChanged:\npreviewCbxOn="+previewCheckboxOn+"; previewThread="+previewThread);
            if (previewCheckboxOn  && previewThread == null) {
                bgPreviewOn = true;                     //need to start a background thread for preview
                previewThread = new Thread(this, command+" Preview");
                int priority = Thread.currentThread().getPriority() - 2;
                if (priority < Thread.MIN_PRIORITY) priority = Thread.MIN_PRIORITY;
                previewThread.setPriority(priority);    //preview on lower priority than dialog
                previewThread.start();
                if (IJ.debugMode) IJ.log(command+" Preview thread was started");
                return true;
            }
            if (previewThread != null) {                //thread runs already
                if (!previewCheckboxOn) {               //preview toggled off
                    killPreview();
                    return true;
                } else
                previewThread.interrupt();              //all other changes: restart calculating preview (with new parameters)
            }
            return true;
        }
    }

| PlugInFilterRunner |