package ij.plugin.frame;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.awt.List;
import java.util.zip.*;
import java.awt.geom.*;

import javax.swing.DefaultListModel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import ij.*;
import ij.process.*;
import ij.gui.*;
import ij.io.*;
import ij.plugin.filter.*;
import ij.plugin.Colors;
import ij.plugin.OverlayLabels;
import ij.util.*;
import ij.macro.*;
import ij.measure.*;
import ij.plugin.OverlayCommands;

/** This plugin implements the Analyze/Tools/ROI Manager command. */
public class RoiManager extends PlugInFrame implements ActionListener, ItemListener, MouseListener, MouseWheelListener, ListSelectionListener {
    public static final String LOC_KEY = "manager.loc";
    private static final int BUTTONS = 11;
    private static final int DRAW=0, FILL=1, LABEL=2;
    private static final int SHOW_ALL=0, SHOW_NONE=1, LABELS=2, NO_LABELS=3;
    private static final int MENU=0, COMMAND=1;
    private static final int IGNORE_POSITION=-999;
    private static final int CHANNEL=0, SLICE=1, FRAME=2, SHOW_DIALOG=3;
    private static int rows = 15;
    private static int lastNonShiftClick = -1;
    private static boolean allowMultipleSelections = true; 
    private static String moreButtonLabel = "More "+'\u00bb';
    private Panel panel;
    private static Frame instance;
    private static int colorIndex = 4;
    private JList list;
    private DefaultListModel listModel;
    private ArrayList rois = new ArrayList();
    private boolean canceled;
    private boolean macro;
    private boolean ignoreInterrupts;
    private PopupMenu pm;
    private Button moreButton, colorButton;
    private Checkbox showAllCheckbox = new Checkbox("Show All", false);
    private Checkbox labelsCheckbox = new Checkbox("Labels", false);

    private static boolean measureAll = true;
    private static boolean onePerSlice = true;
    private static boolean restoreCentered;
    private int prevID;
    private boolean noUpdateMode;
    private int defaultLineWidth = 1;
    private Color defaultColor;
    private boolean firstTime = true;
    private int[] selectedIndexes;
    private boolean appendResults;
    private static ResultsTable mmResults, mmResults2;
    private int imageID;
    private boolean allowRecording;
    private boolean recordShowAll = true;
        
    /** Opens the "ROI Manager" window, or activates it if it is already open.
     * @see #RoiManager(boolean)
     * @see #getRoiManager
    */
    public RoiManager() {
        super("ROI Manager");
        if (instance!=null) {
            WindowManager.toFront(instance);
            return;
        }
        if (IJ.isMacro() && Interpreter.getBatchModeRoiManager()!=null) {
            list = new JList();
            listModel = new DefaultListModel();
            list.setModel(listModel);
            return;
        }
        instance = this;
        list = new JList();
        showWindow();
    }
    
    /** Constructs an ROIManager without displaying it. The boolean argument is ignored. */
    public RoiManager(boolean b) {
        super("ROI Manager");
        list = new JList();
        listModel = new DefaultListModel();
        list.setModel(listModel);
    }

    void showWindow() {
        ImageJ ij = IJ.getInstance();
        addKeyListener(ij);
        addMouseListener(this);
        addMouseWheelListener(this);
        WindowManager.addWindow(this);
        //setLayout(new FlowLayout(FlowLayout.CENTER,5,5));
        setLayout(new BorderLayout());
        listModel = new DefaultListModel();
        list.setModel(listModel);
        list.setPrototypeCellValue("0000-0000-0000 ");      
        list.addListSelectionListener(this);
        list.addKeyListener(ij);
        list.addMouseListener(this);
        list.addMouseWheelListener(this);
        if (IJ.isLinux()) list.setBackground(Color.white);
        JScrollPane scrollPane = new JScrollPane(list, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        add("Center", scrollPane);
        panel = new Panel();
        int nButtons = BUTTONS;
        panel.setLayout(new GridLayout(nButtons, 1, 5, 0));
        addButton("Add [t]");
        addButton("Update");
        addButton("Delete");
        addButton("Rename...");
        addButton("Measure");
        addButton("Deselect");
        addButton("Properties...");
        addButton("Flatten [F]");
        addButton(moreButtonLabel);
        showAllCheckbox.addItemListener(this);
        panel.add(showAllCheckbox);
        labelsCheckbox.addItemListener(this);
        panel.add(labelsCheckbox);
        add("East", panel);     
        addPopupMenu();
        pack();
        Dimension size = getSize();
        if (size.width>270)
            setSize(size.width-40, size.height);
        list.remove(0);
        Point loc = Prefs.getLocation(LOC_KEY);
        if (loc!=null)
            setLocation(loc);
        else
            GUI.center(this);
        show();
    }

    void addButton(String label) {
        Button b = new Button(label);
        b.addActionListener(this);
        b.addKeyListener(IJ.getInstance());
        b.addMouseListener(this);
        if (label.equals(moreButtonLabel)) moreButton = b;
        panel.add(b);
    }

    void addPopupMenu() {
        pm=new PopupMenu();
        //addPopupItem("Select All");
        addPopupItem("Open...");
        addPopupItem("Save...");
        addPopupItem("Fill");
        addPopupItem("Draw");
        addPopupItem("AND");
        addPopupItem("OR (Combine)");
        addPopupItem("XOR");
        addPopupItem("Split");
        addPopupItem("Add Particles");
        addPopupItem("Multi Measure");
        addPopupItem("Multi Plot");
        addPopupItem("Sort");
        addPopupItem("Specify...");
        addPopupItem("Remove Positions...");
        addPopupItem("Labels...");
        addPopupItem("List");
        addPopupItem("Interpolate ROIs");
        addPopupItem("Translate...");
        addPopupItem("Help");
        addPopupItem("Options...");
        add(pm);
    }

    void addPopupItem(String s) {
        MenuItem mi=new MenuItem(s);
        mi.addActionListener(this);
        pm.add(mi);
    }
    
    public void actionPerformed(ActionEvent e) {
        String label = e.getActionCommand();
        if (label==null)
            return;
        String command = label;
        allowRecording = true;
        if (command.equals("Add [t]"))
            runCommand("add");
        else if (command.equals("Update"))
            update(true);
        else if (command.equals("Delete"))
            delete(false);
        else if (command.equals("Rename..."))
            rename(null);
        else if (command.equals("Properties..."))
            setProperties(null, -1, null);
        else if (command.equals("Flatten [F]"))
            flatten();
        else if (command.equals("Measure"))
            measure(MENU);
        else if (command.equals("Open..."))
            open(null);
        else if (command.equals("Save...")) {
            Thread t1 = new Thread(new Runnable() {
                public void run() {save();}
            });  
            t1.start();
        } else if (command.equals("Fill"))
            drawOrFill(FILL);
        else if (command.equals("Draw"))
            drawOrFill(DRAW);
        else if (command.equals("Deselect"))
            deselect();
        else if (command.equals(moreButtonLabel)) {
            Point ploc = panel.getLocation();
            Point bloc = moreButton.getLocation();
            pm.show(this, ploc.x, bloc.y);
        } else if (command.equals("OR (Combine)")) {
            new MacroRunner("roiManager(\"Combine\");");
            if (Recorder.record) Recorder.record("roiManager", "Combine");
        } else if (command.equals("Split"))
            split();
        else if (command.equals("AND"))
            and();
        else if (command.equals("XOR"))
            xor();
        else if (command.equals("Add Particles"))
            addParticles();
        else if (command.equals("Multi Measure"))
            multiMeasure("");
        else if (command.equals("Multi Plot"))
            multiPlot();
        else if (command.equals("Sort"))
            sort();
        else if (command.equals("Specify..."))
            specify();
        else if (command.equals("Remove Positions..."))
            removePositions(SHOW_DIALOG);
        else if (command.equals("Labels..."))
            labels();
        else if (command.equals("List"))
            listRois();
        else if (command.equals("Interpolate ROIs"))
            interpolateRois();
        else if (command.equals("Translate..."))
            translate();
        else if (command.equals("Help"))
            help();
        else if (command.equals("Options..."))
            options();
        else if (command.equals("\"Show All\" Color..."))
            setShowAllColor();
        allowRecording = false;
    }
    
    private void interpolateRois() {
        IJ.runPlugIn("ij.plugin.RoiInterpolator", "");
        if (record())
            Recorder.record("roiManager", "Interpolate ROIs");
    }

    public void itemStateChanged(ItemEvent e) {
        Object source = e.getSource();
        boolean showAllMode = showAllCheckbox.getState();
        if (source==showAllCheckbox) {
            if (firstTime && okToSet())
                labelsCheckbox.setState(true);
            showAll(showAllCheckbox.getState()?SHOW_ALL:SHOW_NONE);
            if (Recorder.record && recordShowAll) {
                if (showAllMode)
                        Recorder.record("roiManager", "Show All");
                    else
                        Recorder.record("roiManager", "Show None");
            }
            recordShowAll = true;
            firstTime = false;
            return;
        }
        if (source==labelsCheckbox) {
            if (firstTime && okToSet())
                showAllCheckbox.setState(true);
            boolean editState = labelsCheckbox.getState();
            boolean showAllState = showAllCheckbox.getState();
            if (!showAllState && !editState)
                showAll(SHOW_NONE);
            else {
                showAll(editState?LABELS:NO_LABELS);
                if (Recorder.record) {
                    if (editState)
                        Recorder.record("roiManager", "Show All with labels");
                    else if (showAllState)
                        Recorder.record("roiManager", "Show All without labels");
                }
                if (editState && !showAllState && okToSet()) {
                    showAllCheckbox.setState(true);
                    recordShowAll = false;
                }
            }
            firstTime = false;
            return;
        }
    }
    
    private boolean okToSet() {
        return !(IJ.isMacOSX()&&IJ.isJava18());
    }
    
    void add(boolean shiftKeyDown, boolean altKeyDown) {
        if (shiftKeyDown)
            addAndDraw(altKeyDown);
        else if (altKeyDown)
            addRoi(true);
        else
            addRoi(false);
    }
    
    /** Adds the specified ROI. */
    public void addRoi(Roi roi) {
        addRoi(roi, false, null, -1);
    }
    
    boolean addRoi(boolean promptForName) {
        return addRoi(null, promptForName, null, IGNORE_POSITION);
    }

    boolean addRoi(Roi roi, boolean promptForName, Color color, int lineWidth) {
        if (listModel==null)
            IJ.log("<<Error: Uninitialized RoiManager>>");
        ImagePlus imp = roi==null?getImage():WindowManager.getCurrentImage();
        if (roi==null) {
            if (imp==null)
                return false;
            roi = imp.getRoi();
            if (roi==null) {
                error("The active image does not have a selection.");
                return false;
            }
        }
        if ((roi instanceof PolygonRoi) && ((PolygonRoi)roi).getNCoordinates()==0)
            return false;
        if (color==null && roi.getStrokeColor()!=null)
            color = roi.getStrokeColor();
        else if (color==null && defaultColor!=null)
            color = defaultColor;
        boolean ignorePosition = false;
        if (lineWidth==IGNORE_POSITION) {
            ignorePosition = true;
            lineWidth = -1;
        }
        if (lineWidth<0) {
            int sw = (int)roi.getStrokeWidth();
            lineWidth = sw>1?sw:defaultLineWidth;
        }
        if (lineWidth>100) lineWidth = 1;
        int n = getCount();
        int position = imp!=null&&!ignorePosition?roi.getPosition():0;
        int saveCurrentSlice = imp!=null?imp.getCurrentSlice():0;
        if (position>0 && position!=saveCurrentSlice)
            imp.setSliceWithoutUpdate(position);
        else
            position = 0;
        if (n>0 && !IJ.isMacro() && imp!=null) {
            // check for duplicate
            Roi roi2 = (Roi)rois.get(n-1);
            if (roi2!=null) {
                String label = (String)listModel.getElementAt(n-1);
                int slice2 = getSliceNumber(roi2, label);
                if (roi.equals(roi2) && (slice2==-1||slice2==imp.getCurrentSlice()) && imp.getID()==prevID && !Interpreter.isBatchMode()) {
                    if (position>0)
                        imp.setSliceWithoutUpdate(saveCurrentSlice);
                    return false;
                }
            }
        }
        prevID = imp!=null?imp.getID():0;
        String name = roi.getName();
        if (isStandardName(name))
            name = null;
        String label = name!=null?name:getLabel(imp, roi, -1);
        if (promptForName)
            label = promptForName(label);
        if (label==null) {
            if (position>0)
                imp.setSliceWithoutUpdate(saveCurrentSlice);
            return false;
        }
        listModel.addElement(label);
        roi.setName(label);
        Roi roiCopy = (Roi)roi.clone();
        roiCopy.setPosition(imp);
        if (lineWidth>1)
            roiCopy.setStrokeWidth(lineWidth);
        if (color!=null)
            roiCopy.setStrokeColor(color);
        rois.add(roiCopy);
        updateShowAll();
        if (record())
            recordAdd(defaultColor, defaultLineWidth);
        if (position>0)
            imp.setSliceWithoutUpdate(saveCurrentSlice);
        return true;
    }
        
    void recordAdd(Color color, int lineWidth) {
        if (Recorder.scriptMode())
            Recorder.recordCall("rm.addRoi(imp.getRoi());");
        else if (color!=null && lineWidth==1)
            Recorder.recordString("roiManager(\"Add\", \""+getHex(color)+"\");\n");
        else if (lineWidth>1)
            Recorder.recordString("roiManager(\"Add\", \""+getHex(color)+"\", "+lineWidth+");\n");
        else
            Recorder.record("roiManager", "Add");
    }
    
    String getHex(Color color) {
        if (color==null) color = ImageCanvas.getShowAllColor();
        String hex = Integer.toHexString(color.getRGB());
        if (hex.length()==8) hex = hex.substring(2);
        return hex;
    }
    
    /** Adds the specified ROI to the list. The third argument ('n') will 
        be used to form the first part of the ROI label if it is >= 0. */
    public void add(ImagePlus imp, Roi roi, int n) {
        if (IJ.debugMode && n<3 && roi!=null) IJ.log("RoiManager.add: "+n+" "+roi.getName());
        if (roi==null)
            return;
        String label = roi.getName();
        String label2 = label;
        if (label==null)
            label = getLabel(imp, roi, n);
        else
            label = label+"-"+n;
        if (label==null)
            return;
        listModel.addElement(label);
        if (label2!=null)
            roi.setName(label2);
        else
            roi.setName(label);
        rois.add((Roi)roi.clone());
    }

    boolean isStandardName(String name) {
        if (name==null)
            return false;
        int len = name.length();
        if (len<9 || (len>0&&!Character.isDigit(name.charAt(0))))
            return false;
        boolean isStandard = false;
        if (len>=14 && name.charAt(4)=='-' && name.charAt(9)=='-' )
            isStandard = true;
        else if (len>=17 && name.charAt(5)=='-' && name.charAt(11)=='-' )
            isStandard = true;
        else if (len>=9 && name.charAt(4)=='-')
            isStandard = true;
        else if (len>=11 && name.charAt(5)=='-')
            isStandard = true;
        return isStandard;
    }
    
    String getLabel(ImagePlus imp, Roi roi, int n) {
        Rectangle r = roi.getBounds();
        int xc = r.x + r.width/2;
        int yc = r.y + r.height/2;
        if (n>=0)
            {xc = yc; yc=n;}
        if (xc<0) xc = 0;
        if (yc<0) yc = 0;
        int digits = 4;
        String xs = "" + xc;
        if (xs.length()>digits) digits = xs.length();
        String ys = "" + yc;
        if (ys.length()>digits) digits = ys.length();
        if (digits==4 && imp!=null && (imp.getStackSize()>=10000||imp.getHeight()>=10000))
            digits = 5;
        xs = "000000" + xc;
        ys = "000000" + yc;
        String label = ys.substring(ys.length()-digits) + "-" + xs.substring(xs.length()-digits);
        if (imp!=null && imp.getStackSize()>1) {
            int slice = imp.getCurrentSlice();
            String zs = "000000" + slice;
            label = zs.substring(zs.length()-digits) + "-" + label;
        }
        return label;
    }

    void addAndDraw(boolean altKeyDown) {
        if (altKeyDown) {
            if (!addRoi(true)) return;
        } else if (!addRoi(false))
            return;
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp!=null) {
            Undo.setup(Undo.COMPOUND_FILTER, imp);
            IJ.run(imp, "Draw", "slice");
            Undo.setup(Undo.COMPOUND_FILTER_DONE, imp);
        }
        if (record()) Recorder.record("roiManager", "Add & Draw");
    }
    
    boolean delete(boolean replacing) {
        int count = getCount();
        if (count==0)
            return error("The ROI Manager is empty.");
        int index[] = getSelectedIndexes();
        if (index.length==0 || (replacing&&count>1)) {
            String msg = "Delete all items on the list?";
            if (replacing)
                msg = "Replace items on the list?";
            canceled = false;
            if (!IJ.isMacro() && !macro) {
                YesNoCancelDialog d = new YesNoCancelDialog(this, "ROI Manager", msg);
                if (d.cancelPressed())
                    {canceled = true; return false;}
                if (!d.yesPressed()) return false;
            }
            index = getAllIndexes();
        }
        if (count==index.length && !replacing) {
            rois.clear();
            listModel.removeAllElements();
        } else {
            for (int i=count-1; i>=0; i--) {
                boolean delete = false;
                for (int j=0; j<index.length; j++) {
                    if (index[j]==i)
                        delete = true;
                }
                if (delete) {
                    rois.remove(i);
                    listModel.remove(i);
                }
            }
        }
        ImagePlus imp = WindowManager.getCurrentImage();
        if (count>1 && index.length==1 && imp!=null)
            imp.deleteRoi();
        updateShowAll();
        if (record()) Recorder.record("roiManager", "Delete");
        return true;
    }
    
    boolean update(boolean clone) {
        ImagePlus imp = getImage();
        if (imp==null)
            return false;
        ImageCanvas ic = imp.getCanvas();
        boolean showingAll = ic!=null &&  ic.getShowAllROIs();
        Roi roi = imp.getRoi();
        if (roi==null) {
            error("The active image does not have a selection.");
            return false;
        }
        int index = list.getSelectedIndex();
        if (index<0 && !showingAll)
            return error("Exactly one item in the list must be selected.");
        if (index>=0) {
            if (clone) {
                String name = (String)listModel.getElementAt(index);
                Roi roi2 = (Roi)roi.clone();
                roi2.setPosition(imp);
                roi.setName(name);
                roi2.setName(name);
                rois.set(index, roi2);
            } else
                rois.set(index, roi);
        }
        if (record()) Recorder.record("roiManager", "Update");
        updateShowAll();
        return true;
    }

    boolean rename(String name2) {
        int index = list.getSelectedIndex();
        if (index<0)
            return error("Exactly one item in the list must be selected.");
        String name = (String)listModel.getElementAt(index);
        if (name2==null)
            name2 = promptForName(name);
        if (name2==null)
            return false;
        if (name2.equals(name))
            return false;
        Roi roi = (Roi)rois.get(index);
        roi.setName(name2);
        int position = getSliceNumber(name2);
        if (position>0 && !roi.hasHyperStackPosition())
            roi.setPosition(position);
        rois.set(index, roi);
        listModel.setElementAt(name2, index);
        list.setSelectedIndex(index);
        if (Prefs.useNamesAsLabels && labelsCheckbox.getState()) {
            ImagePlus imp = WindowManager.getCurrentImage();
            if (imp!=null) imp.draw();
        }
        if (record())
            Recorder.record("roiManager", "Rename", name2);
        return true;
    }
    
    public void rename(int index, String newName) {
        if (index<0 || index>=getCount())
            throw new IllegalArgumentException("Index out of range: "+index);
        Roi roi = (Roi)rois.get(index);
        roi.setName(newName);
        listModel.setElementAt(newName, index);
    }

    String promptForName(String name) {
        GenericDialog gd = new GenericDialog("ROI Manager");
        gd.addStringField("Rename As:", name, 20);
        gd.showDialog();
        if (gd.wasCanceled())
            return null;
        else
            return gd.getNextString();
    }

    boolean restore(ImagePlus imp, int index, boolean setSlice) {
        Roi roi = (Roi)rois.get(index);
        if (imp==null || roi==null)
            return false;
        if (setSlice) {
            boolean hyperstack = imp.isHyperStack();
            if (hyperstack && roi.hasHyperStackPosition())
                imp.setPosition(roi.getCPosition(), roi.getZPosition(), roi.getTPosition());
            else {
                String label = (String)listModel.getElementAt(index);
                int n = getSliceNumber(roi, label);
                if (n>=1 && n<=imp.getStackSize()) {
                    if (hyperstack) {
                        if (imp.getNSlices()>1 && n<=imp.getNSlices())
                            imp.setPosition(imp.getC(),n,imp.getT());
                        else if (imp.getNFrames()>1 && n<=imp.getNFrames())
                            imp.setPosition(imp.getC(),imp.getZ(),n);
                        else
                            imp.setPosition(n);
                    } else
                        imp.setSlice(n);
                }
            }
        }
        if (showAllCheckbox.getState() && !restoreCentered && !noUpdateMode) {
            roi.setImage(null);
            imp.setRoi(roi);
            return true;
        }
        Roi roi2 = (Roi)roi.clone();
        Rectangle r = roi2.getBounds();
        int width= imp.getWidth(), height=imp.getHeight();
        if (restoreCentered) {
            ImageCanvas ic = imp.getCanvas();
            if (ic!=null) {
                Rectangle r1 = ic.getSrcRect();
                Rectangle r2 = roi2.getBounds();
                roi2.setLocation(r1.x+r1.width/2-r2.width/2, r1.y+r1.height/2-r2.height/2);
            }
        }
        if (r.x>=width || r.y>=height || (r.x+r.width)<0 || (r.y+r.height)<0) {
            if (roi2.getType()!=Roi.POINT)
                roi2.setLocation((width-r.width)/2, (height-r.height)/2);
        }
        if (noUpdateMode) {
            imp.setRoi(roi2, false);
            noUpdateMode = false;
        } else
            imp.setRoi(roi2, true);
        return true;
    }
    
    private boolean restoreWithoutUpdate(ImagePlus imp, int index) {
        noUpdateMode = true;
        if (imp==null)
            imp = getImage();
        return restore(imp, index, false);
    }
    
    /** Returns the slice number associated with the specified name,
        or -1 if the name does not include a slice number. */
    public int getSliceNumber(String label) {
        int slice = -1;
        if (label.length()>=14 && label.charAt(4)=='-' && label.charAt(9)=='-')
            slice = (int)Tools.parseDouble(label.substring(0,4),-1);
        else if (label.length()>=17 && label.charAt(5)=='-' && label.charAt(11)=='-')
            slice = (int)Tools.parseDouble(label.substring(0,5),-1);
        else if (label.length()>=20 && label.charAt(6)=='-' && label.charAt(13)=='-')
            slice = (int)Tools.parseDouble(label.substring(0,6),-1);
        return slice;
    }
    
    /** Returns the slice number associated with the specified ROI or name,
        or -1 if the ROI or name does not include a slice number. */
    int getSliceNumber(Roi roi, String label) {
        int slice = roi!=null?roi.getPosition():-1;
        if (slice==0)
            slice=-1;
        if (slice==-1)
            slice = getSliceNumber(label);
        return slice;
    }

    void open(String path) {
        Macro.setOptions(null);
        String name = null;
        if (path==null || path.equals("")) {
            OpenDialog od = new OpenDialog("Open Selection(s)...", "");
            String directory = od.getDirectory();
            name = od.getFileName();
            if (name==null)
                return;
            path = directory + name;
        }
        if (Recorder.record && !Recorder.scriptMode())
            Recorder.record("roiManager", "Open", path);
        if (path.endsWith(".zip")) {
            openZip(path);
            return;
        }
        Opener o = new Opener();
        if (name==null) name = o.getName(path);
        Roi roi = o.openRoi(path);
        if (roi!=null) {
            if (name.endsWith(".roi"))
                name = name.substring(0, name.length()-4);
            listModel.addElement(name);
            rois.add(roi);
        }       
        updateShowAll();
    }
    
    // Modified on 2005/11/15 by Ulrik Stervbo to only read .roi files and to not empty the current list
    void openZip(String path) { 
        ZipInputStream in = null; 
        ByteArrayOutputStream out = null; 
        int nRois = 0; 
        try { 
            in = new ZipInputStream(new FileInputStream(path)); 
            byte[] buf = new byte[1024]; 
            int len; 
            ZipEntry entry = in.getNextEntry(); 
            while (entry!=null) { 
                String name = entry.getName();
                if (name.endsWith(".roi")) { 
                    out = new ByteArrayOutputStream(); 
                    while ((len = in.read(buf)) > 0) 
                        out.write(buf, 0, len); 
                    out.close(); 
                    byte[] bytes = out.toByteArray(); 
                    RoiDecoder rd = new RoiDecoder(bytes, name); 
                    Roi roi = rd.getRoi(); 
                    if (roi!=null) { 
                        name = name.substring(0, name.length()-4); 
                        listModel.addElement(name); 
                        rois.add(roi); 
                        nRois++;
                    } 
                } 
                entry = in.getNextEntry(); 
            } 
            in.close(); 
        } catch (IOException e) {
            error(e.toString());
        } finally {
            if (in!=null)
                try {in.close();} catch (IOException e) {}
            if (out!=null)
                try {out.close();} catch (IOException e) {}
        }
        if(nRois==0)
                error("This ZIP archive does not appear to contain \".roi\" files");
        updateShowAll();
    } 

    boolean save() {
        if (getCount()==0)
            return error("The selection list is empty.");
        int[] indexes = getIndexes();
        if (indexes.length>1)
            return saveMultiple(indexes, null);
        String name = (String) listModel.getElementAt(indexes[0]);
        Macro.setOptions(null);
        SaveDialog sd = new SaveDialog("Save Selection...", name, ".roi");
        String name2 = sd.getFileName();
        if (name2 == null)
            return false;
        String dir = sd.getDirectory();
        Roi roi = (Roi)rois.get(indexes[0]);
        if (!name2.endsWith(".roi")) name2 = name2+".roi";
        String newName = name2.substring(0, name2.length()-4);
        rois.set(indexes[0], roi);
        roi.setName(newName);
        listModel.setElementAt(newName, indexes[0]);
        RoiEncoder re = new RoiEncoder(dir+name2);
        try {
            re.write(roi);
        } catch (IOException e) {
            IJ.error("ROI Manager", e.getMessage());
        }
        if (record()) {
            String path = dir+name2;
            if (Recorder.scriptMode())
                Recorder.recordCall("IJ.saveAs(imp, \"Selection\", \""+path+"\");");
            else
                Recorder.record("saveAs", "Selection", path);
        }
        return true;
    }

    boolean saveMultiple(int[] indexes, String path) {
        Macro.setOptions(null);
        if (path==null) {
            SaveDialog sd = new SaveDialog("Save ROIs...", "RoiSet", ".zip");
            String name = sd.getFileName();
            if (name == null)
                return false;
            if (!(name.endsWith(".zip") || name.endsWith(".ZIP")))
                name = name + ".zip";
            String dir = sd.getDirectory();
            path = dir+name;
        }
        DataOutputStream out = null;
        IJ.showStatus("Saving "+indexes.length+" ROIs "+" to "+path);
        long t0 = System.currentTimeMillis();
        String[] names = new String[listModel.size()];
        for (int i=0; i<listModel.size(); i++)
            names[i] = (String)listModel.getElementAt(i);
        try {
            ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(path)));
            out = new DataOutputStream(new BufferedOutputStream(zos));
            RoiEncoder re = new RoiEncoder(out);
            for (int i=0; i<indexes.length; i++) {
                IJ.showProgress(i, indexes.length);
                String label = getUniqueName(names, indexes[i]);
                Roi roi = (Roi)rois.get(indexes[i]);
                if (IJ.debugMode) IJ.log("saveMultiple: "+i+"  "+label+"  "+roi);
                if (roi==null) continue;
                if (!label.endsWith(".roi")) label += ".roi";
                zos.putNextEntry(new ZipEntry(label));
                re.write(roi);
                out.flush();
            }
            out.close();
        } catch (IOException e) {
            error(""+e);
            return false;
        } finally {
            if (out!=null)
                try {out.close();} catch (IOException e) {}
        }
        double time = (System.currentTimeMillis()-t0)/1000.0;
        IJ.showProgress(1.0);
        IJ.showStatus(IJ.d2s(time,3)+" seconds, "+indexes.length+" ROIs, "+path);
        if (Recorder.record && !IJ.isMacro())
            Recorder.record("roiManager", "Save", path);
        return true;
    }
    
    String getUniqueName(String[] names, int index) {
        String name = names[index];
        int n = 1;
        int index2 = getIndex(names, index, name);
        while (index2!=-1) {
            index2 = getIndex(names, index, name);
            if (index2!=-1) {
                int lastDash = name.lastIndexOf("-");
                if (lastDash!=-1 && name.length()-lastDash<5)
                    name = name.substring(0, lastDash);
                name = name+"-"+n;
                n++;
            }
            index2 = getIndex(names, index, name);
        }
        names[index] = name;
        return name;
    }
    
    private int getIndex(String[] names, int index, String name) {
        int index2 = -1;
        for (int i=0; i<names.length; i++) {
            if (i!=index && names[i].equals(name))
            return i;
        }
        return index2;
    }

    private void listRois() {
        Roi[] list = getRoisAsArray();
        OverlayCommands.listRois(list);
        if (record())
            Recorder.record("roiManager", "List");
    }
        
    boolean measure(int mode) {
        ImagePlus imp = getImage();
        if (imp==null)
            return false;
        int[] indexes = getIndexes();
        if (indexes.length==0) return false;
        boolean allSliceOne = true;
        for (int i=0; i<indexes.length; i++) {
            Roi roi = (Roi)rois.get(indexes[i]);
            String label = (String) listModel.getElementAt(indexes[i]);
            if (getSliceNumber(roi,label)>1) allSliceOne=false;
        }
        int measurements = Analyzer.getMeasurements();
        if (imp.getStackSize()>1)
            Analyzer.setMeasurements(measurements|Measurements.SLICE);
        int currentSlice = imp.getCurrentSlice();
        Analyzer.setMeasurements(measurements&(~Measurements.ADD_TO_OVERLAY));
        for (int i=0; i<indexes.length; i++) {
            if (restore(getImage(), indexes[i], !allSliceOne))
                IJ.run("Measure");
            else
                break;
        }
        Analyzer.setMeasurements(measurements);
        imp.setSlice(currentSlice);
        if (indexes.length>1)
            IJ.run("Select None");
        if (record()) Recorder.record("roiManager", "Measure");
        return true;
    }   
    
    /** This method measures the selected ROIs, or all ROIs if
     * none are selected, on all the slices of a stack and returns
     * a ResultsTable arranged with one row per slice.
     * @see <a href="/ij/macros/js/MultiMeasureDemo.js">JavaScript example</a>
    */
    public ResultsTable multiMeasure(ImagePlus imp) {
        Roi[] rois = getSelectedRoisAsArray();
        ResultsTable rt = multiMeasure(imp, rois, false);
        imp.deleteRoi();
        return rt;
    }
    
    /** This method performs measurements for several ROI's in a stack
        and arranges the results with one line per slice.  By contrast, the 
        measure() method produces several lines per slice.  The results 
        from multiMeasure() may be easier to import into a spreadsheet 
        program for plotting or additional analysis. Based on the multi() 
        method in Bob Dougherty's Multi_Measure plugin
        (http://www.optinav.com/Multi-Measure.htm).
    */
    boolean multiMeasure(String cmd) {
        ImagePlus imp = getImage();
        if (imp==null) return false;
        int[] indexes = getIndexes();
        if (indexes.length==0)
            return false;
        int measurements = Analyzer.getMeasurements();

        int nSlices = imp.getStackSize();
        if (cmd!=null)
            appendResults = cmd.contains("append")?true:false;
        if (IJ.isMacro()) {
            if (cmd.startsWith("multi-measure")) {
                measureAll = cmd.contains(" measure") && nSlices>1; // measure-all
                onePerSlice = cmd.contains(" one");
                appendResults = cmd.contains(" append");
            } else {
                if (nSlices>1)
                    measureAll = true;
                onePerSlice = true;
            }
        } else {
            GenericDialog gd = new GenericDialog("Multi Measure");
            if (nSlices>1)
                gd.addCheckbox("Measure all "+nSlices+" slices", measureAll);
            gd.addCheckbox("One row per slice", onePerSlice);
            gd.addCheckbox("Append results", appendResults);
            int columns = getColumnCount(imp, measurements)*indexes.length;
            String str = nSlices==1?"this option":"both options";
            gd.setInsets(10, 25, 0);
            gd.addMessage(
                "Enabling "+str+" will result\n"+
                "in a table with "+columns+" columns."
            );
            gd.showDialog();
            if (gd.wasCanceled()) return false;
            if (nSlices>1)
                measureAll = gd.getNextBoolean();
            onePerSlice = gd.getNextBoolean();
            appendResults = gd.getNextBoolean();
        }
        if (!measureAll) nSlices = 1;
        int currentSlice = imp.getCurrentSlice();
        
        if (!onePerSlice) {
            int measurements2 = nSlices>1?measurements|Measurements.SLICE:measurements;
            ResultsTable rt = new ResultsTable();
            if (appendResults && mmResults2!=null)
                rt = mmResults2;
            Analyzer analyzer = new Analyzer(imp, measurements2, rt);
            analyzer.disableReset(true);
            for (int slice=1; slice<=nSlices; slice++) {
                if (nSlices>1) imp.setSliceWithoutUpdate(slice);
                for (int i=0; i<indexes.length; i++) {
                    if (restoreWithoutUpdate(imp, indexes[i]))
                        analyzer.measure();
                    else
                        break;
                }
            }
            mmResults2 = (ResultsTable)rt.clone();
            rt.show("Results");
            if (nSlices>1)
                imp.setSlice(currentSlice);
        } else {
            Roi[] rois = getSelectedRoisAsArray();
            if ("".equals(cmd)) { // run More>>Multi Measure command in separate thread
                MultiMeasureRunner mmr = new MultiMeasureRunner();
                mmr.multiMeasure(imp, rois, appendResults);
            } else {
                ResultsTable rtMulti = multiMeasure(imp, rois, appendResults);
                mmResults = (ResultsTable)rtMulti.clone();
                rtMulti.show("Results");
                imp.setSlice(currentSlice);
                if (indexes.length>1)
                    IJ.run("Select None");
            }
        }
        if (record()) {
            if (Recorder.scriptMode()) {
                Recorder.recordCall("rt = rm.multiMeasure(imp);");
                Recorder.recordCall("rt.show(\"Results\");");
            } else {
                if ((nSlices==1||measureAll) && onePerSlice && !appendResults)
                    Recorder.record("roiManager", "Multi Measure");
                else {
                    String options = "";
                    if (measureAll)
                        options += " measure_all";
                    if (onePerSlice)
                        options += " one";
                    if (appendResults)
                        options += " append";
                    Recorder.record("roiManager", "multi-measure"+options);
                }
            }
        }
        return true;
    }
    
    private static ResultsTable multiMeasure(ImagePlus imp, Roi[] rois, boolean appendResults) {
        int nSlices = imp.getStackSize();
        Analyzer aSys = new Analyzer(imp); // System Analyzer
        ResultsTable rtSys = Analyzer.getResultsTable();
        ResultsTable rtMulti = new ResultsTable();
        if (appendResults && mmResults!=null)
            rtMulti = mmResults;
        rtSys.reset();
        int currentSlice = imp.getCurrentSlice();
        for (int slice=1; slice<=nSlices; slice++) {
            int sliceUse = slice;
            if (nSlices==1) sliceUse = currentSlice;
            imp.setSliceWithoutUpdate(sliceUse);
            rtMulti.incrementCounter();
            if ((Analyzer.getMeasurements()&Measurements.LABELS)!=0)
                rtMulti.addLabel("Label", imp.getTitle());
            int roiIndex = 0;
            for (int i=0; i<rois.length; i++) {
                imp.setRoi(rois[i]);
                roiIndex++;
                aSys.measure();
                for (int j=0; j<=rtSys.getLastColumn(); j++){
                    float[] col = rtSys.getColumn(j);
                    String head = rtSys.getColumnHeading(j);
                    String suffix = ""+roiIndex;
                    Roi roi = imp.getRoi();
                    if (roi!=null) {
                        String name = roi.getName();
                        if (name!=null && name.length()>0 && (name.length()<9||!Character.isDigit(name.charAt(0))))
                            suffix = "("+name+")";
                    }
                    if (head!=null && col!=null && !head.equals("Slice"))
                        rtMulti.addValue(head+suffix, rtSys.getValue(j,rtSys.getCounter()-1));
                }
            }
            if (nSlices>1) IJ.showProgress(slice,nSlices);
        }
        return rtMulti;
    }

    int getColumnCount(ImagePlus imp, int measurements) {
        ImageStatistics stats = imp.getStatistics(measurements);
        ResultsTable rt = new ResultsTable();
        Analyzer analyzer = new Analyzer(imp, measurements, rt);
        analyzer.saveResults(stats, null);
        int count = 0;
        for (int i=0; i<=rt.getLastColumn(); i++) {
            float[] col = rt.getColumn(i);
            String head = rt.getColumnHeading(i);
            if (head!=null && col!=null)
                count++;
        }
        return count;
    }
    
    void multiPlot() {
        ImagePlus imp = getImage();
        if (imp==null) return;
        int[] indexes = getIndexes();
        int n = indexes.length;
        if (n==0) return;
        Color[] colors = {Color.blue, Color.green, Color.magenta, Color.red, Color.cyan, Color.yellow};
        if (n>colors.length) {
            colors = new Color[n];
            double c = 0;
            double inc =150.0/n;
            for (int i=0; i<n; i++) {
                colors[i] = new Color((int)c, (int)c, (int)c);
                c += inc;
            }
        }
        int currentSlice = imp.getCurrentSlice();
        double[][] x = new double[n][];
        double[][] y = new double[n][];
        double minY = Double.MAX_VALUE;
        double maxY = -Double.MAX_VALUE;
        double fixedMin = ProfilePlot.getFixedMin();
        double fixedMax = ProfilePlot.getFixedMax();    
        boolean freeYScale = fixedMin==0.0 && fixedMax==0.0;
        if (!freeYScale) {
            minY = fixedMin;
            maxY = fixedMax;
        }
        int maxX = 0;
        Calibration cal = imp.getCalibration();
        double xinc = cal.pixelWidth;
        for (int i=0; i<indexes.length; i++) {
            if (!restore(getImage(), indexes[i], true)) break;
            Roi roi = imp.getRoi();
            if (roi==null) break;
            if (roi.isArea() && roi.getType()!=Roi.RECTANGLE)
                IJ.run(imp, "Area to Line", "");
            ProfilePlot pp = new ProfilePlot(imp, Prefs.verticalProfile||IJ.altKeyDown());
            y[i] = pp.getProfile();
            if (y[i]==null) break;
            if (y[i].length>maxX) maxX = y[i].length;
            if (freeYScale) {
                double[] a = Tools.getMinMax(y[i]);
                if (a[0]<minY) minY=a[0];
                if (a[1]>maxY) maxY = a[1];
            }
            double[] xx = new double[y[i].length];
            for (int j=0; j<xx.length; j++)
                xx[j] = j*xinc;
            x[i] = xx;
        }
        String xlabel = "Distance ("+cal.getUnits()+")";
        Plot plot = new Plot("Profiles",xlabel, "Value", x[0], y[0]);
        plot.setLimits(0, maxX*xinc, minY, maxY);
        for (int i=1; i<indexes.length; i++) {
            plot.setColor(colors[i]);
            if (x[i]!=null)
                plot.addPoints(x[i], y[i], Plot.LINE);
        }
        plot.setColor(colors[0]);
        if (x[0]!=null)
            plot.show();
        imp.setSlice(currentSlice);
        if (indexes.length>1)
            IJ.run("Select None");
        if (record()) Recorder.record("roiManager", "Multi Plot");
    }   

    boolean drawOrFill(int mode) {
        int[] indexes = getIndexes();
        ImagePlus imp = WindowManager.getCurrentImage();
        imp.deleteRoi();
        ImageProcessor ip = imp.getProcessor();
        ip.setColor(Toolbar.getForegroundColor());
        ip.snapshot();
        Undo.setup(Undo.FILTER, imp);
        Filler filler = mode==LABEL?new Filler():null;
        int slice = imp.getCurrentSlice();
        for (int i=0; i<indexes.length; i++) {
            Roi roi = (Roi)rois.get(indexes[i]);
            int type = roi.getType();
            if (roi==null) continue;
            if (mode==FILL&&(type==Roi.POLYLINE||type==Roi.FREELINE||type==Roi.ANGLE))
                mode = DRAW;
            String name = (String) listModel.getElementAt(indexes[i]);
            int slice2 = getSliceNumber(roi, name);
            if (slice2>=1 && slice2<=imp.getStackSize()) {
                imp.setSlice(slice2);
                ip = imp.getProcessor();
                ip.setColor(Toolbar.getForegroundColor());
                if (slice2!=slice) Undo.reset();
            }
            switch (mode) {
                case DRAW: roi.drawPixels(ip); break;
                case FILL: ip.fill(roi); break;
                case LABEL:
                    roi.drawPixels(ip);
                    filler.drawLabel(imp, ip, i+1, roi.getBounds());
                    break;
            }
        }
        if (record() && (mode==DRAW||mode==FILL))
            Recorder.record("roiManager", mode==DRAW?"Draw":"Fill");
        if (showAllCheckbox.getState())
            runCommand("show none");
        imp.updateAndDraw();
        return true;
    }

    void setProperties(Color color, int lineWidth, Color fillColor) {
        boolean showDialog = color==null && lineWidth==-1 && fillColor==null;
        int[] indexes = getIndexes();
        int n = indexes.length;
        if (n==0) return;
        Roi rpRoi = null;
        String rpName = null;
        Font font = null;
        int justification = TextRoi.LEFT;
        double opacity = -1;
        int pointType = -1;
        int pointSize = -1;
        if (showDialog) {
            //String label = (String) listModel.getElementAt(indexes[0]);
            rpRoi = (Roi)rois.get(indexes[0]);
            if (n==1) {
                fillColor =  rpRoi.getFillColor();
                rpName = rpRoi.getName();
            }
            if (rpRoi.getStrokeColor()==null)
                rpRoi.setStrokeColor(Roi.getColor());
            rpRoi = (Roi) rpRoi.clone();
            if (n>1)
                rpRoi.setName("range: "+(indexes[0]+1)+"-"+(indexes[n-1]+1));
            rpRoi.setFillColor(fillColor);
            RoiProperties rp = new RoiProperties("Properties", rpRoi);
            if (!rp.showDialog())
                return;
            lineWidth = (int)rpRoi.getStrokeWidth();
            defaultLineWidth = lineWidth;
            color =  rpRoi.getStrokeColor();
            fillColor =  rpRoi.getFillColor();
            defaultColor = color;
            if (rpRoi instanceof TextRoi) {
                font = ((TextRoi)rpRoi).getCurrentFont();
                justification = ((TextRoi)rpRoi).getJustification();
            }
            if (rpRoi instanceof ImageRoi)
                opacity = ((ImageRoi)rpRoi).getOpacity();
            if (rpRoi instanceof PointRoi) {
                pointType = ((PointRoi)rpRoi).getPointType();
                pointSize = ((PointRoi)rpRoi).getSize();
            }
        }
        ImagePlus imp = WindowManager.getCurrentImage();
        if (n==getCount() && n>1 && !IJ.isMacro()) {
            GenericDialog gd = new GenericDialog("ROI Manager");
            gd.addMessage("Apply changes to all "+n+" selections?");
            gd.showDialog();
            if (gd.wasCanceled()) return;
        }
        for (int i=0; i<n; i++) {
            //String label = (String) listModel.getElementAt(indexes[i]);
            Roi roi = (Roi)rois.get(indexes[i]);
            if (roi==null) continue;
            //IJ.log("set "+color+"  "+lineWidth+"  "+fillColor);
            if (color!=null) roi.setStrokeColor(color);
            if (lineWidth>=0) roi.setStrokeWidth(lineWidth);
            roi.setFillColor(fillColor);
            if (rpRoi!=null) {
                if (rpRoi.hasHyperStackPosition())
                    roi.setPosition(rpRoi.getCPosition(), rpRoi.getZPosition(), rpRoi.getTPosition());
                else
                    roi.setPosition(rpRoi.getPosition());
            }
            if (roi instanceof TextRoi) {
                roi.setImage(imp);
                if (font!=null)
                    ((TextRoi)roi).setCurrentFont(font);
                ((TextRoi)roi).setJustification(justification);
                roi.setImage(null);
            }
            if ((roi instanceof ImageRoi) && opacity!=-1)
                ((ImageRoi)roi).setOpacity(opacity);
            if (roi instanceof PointRoi) {
                if (pointType!=-1) ((PointRoi)roi).setPointType(pointType);
                if (pointSize!=-1) ((PointRoi)roi).setSize(pointSize);
            }
        }
        if (rpRoi!=null && rpName!=null && !rpRoi.getName().equals(rpName))
            rename(rpRoi.getName());
        ImageCanvas ic = imp!=null?imp.getCanvas():null;
        Roi roi = imp!=null?imp.getRoi():null;
        boolean showingAll = ic!=null &&  ic.getShowAllROIs();
        if (roi!=null && (n==1||!showingAll)) {
            if (lineWidth>=0) roi.setStrokeWidth(lineWidth);
            if (color!=null) roi.setStrokeColor(color);
            if (fillColor!=null) roi.setFillColor(fillColor);
            if (roi!=null && (roi instanceof TextRoi)) {
                ((TextRoi)roi).setCurrentFont(font);
                ((TextRoi)roi).setJustification(justification);
            }
            if (roi!=null && (roi instanceof ImageRoi) && opacity!=-1)
                ((ImageRoi)roi).setOpacity(opacity);
        }
        if (lineWidth>1 && !showingAll && roi==null) {
            showAll(SHOW_ALL);
            showingAll = true;
        }
        if (imp!=null) imp.draw();
        if (record()) {
            if (fillColor!=null)
                Recorder.record("roiManager", "Set Fill Color", Colors.colorToString(fillColor));
            else {
                Recorder.record("roiManager", "Set Color", Colors.colorToString(color!=null?color:Color.red));
                Recorder.record("roiManager", "Set Line Width", lineWidth);
            }
        }
    }
    
    void flatten() {
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp==null)
            {IJ.noImage(); return;}
        ImageCanvas ic = imp.getCanvas();
        if ((ic!=null && ic.getShowAllList()==null) && imp.getOverlay()==null && imp.getRoi()==null)
            error("Image does not have an overlay or ROI");
        else
            IJ.doCommand("Flatten"); // run Image>Flatten in separate thread
    }
            
    public boolean getDrawLabels() {
        return labelsCheckbox.getState();
    }

    private void combine() {
        ImagePlus imp = getImage();
        if (imp==null)
            return;
        Roi[] rois = getSelectedRoisAsArray();
        if (rois.length==1) {
            error("More than one item must be selected, or none");
            return;
        }
        int nPointRois = 0;
        for (int i=0; i<rois.length; i++) {
            if (rois[i].getType()==Roi.POINT)
                nPointRois++;
            else
                break;
        }
        if (nPointRois==rois.length)
            combinePoints(imp, rois);
        else
            combineRois(imp, rois);
    }
    
    private void combineRois(ImagePlus imp, Roi[] rois) {
        IJ.resetEscape();
        ShapeRoi s1=null, s2=null;
        ImageProcessor ip = null;
        for (int i=0; i<rois.length; i++) {
            IJ.showProgress(i, rois.length-1);
            if (IJ.escapePressed()) {
                IJ.showProgress(1.0);
                return;
            }
            Roi roi = rois[i];
            if (!roi.isArea()) {
                if (ip==null)
                    ip = new ByteProcessor(imp.getWidth(), imp.getHeight());
                roi = convertLineToPolygon(roi, ip);
                if (roi==null) continue;
            }
            if (s1==null) {
                if (roi instanceof ShapeRoi)
                    s1 = (ShapeRoi)roi;
                else
                    s1 = new ShapeRoi(roi);
                if (s1==null) return;
            } else {
                if (roi instanceof ShapeRoi)
                    s2 = (ShapeRoi)roi;
                else
                    s2 = new ShapeRoi(roi);
                if (s2==null) continue;
                s1.or(s2);
            }
        }
        if (s1!=null)
            imp.setRoi(s1);
    }
    
    Roi convertLineToPolygon(Roi roi, ImageProcessor ip) {
        if (roi==null) return null;
        ip.resetRoi();
        ip.setColor(0);
        ip.fill();
        ip.setColor(255);
        if (roi.getType()==Roi.LINE && roi.getStrokeWidth()>1)
            ip.fillPolygon(roi.getPolygon());
        else
            roi.drawPixels(ip);
        //new ImagePlus("ip", ip.duplicate()).show();
        ip.setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE);
        ThresholdToSelection tts = new ThresholdToSelection();
        return tts.convert(ip);
    }

    void combinePoints(ImagePlus imp, Roi[] rois) {
        int n = rois.length;
        Polygon[] p = new Polygon[n];
        int points = 0;
        for (int i=0; i<n; i++) {
            p[i] = rois[i].getPolygon();
            points += p[i].npoints;
        }
        if (points==0)
            return;
        int[] xpoints = new int[points];
        int[] ypoints = new int[points];
        int index = 0;
        for (int i=0; i<p.length; i++) {
            for (int j=0; j<p[i].npoints; j++) {
                xpoints[index] = p[i].xpoints[j];
                ypoints[index] = p[i].ypoints[j];
                index++;
            }   
        }
        imp.setRoi(new PointRoi(xpoints, ypoints, xpoints.length));
    }

    void and() {
        ImagePlus imp = getImage();
        if (imp==null) return;
        int[] indexes = getSelectedIndexes();
        if (indexes.length==1) {
            error("More than one item must be selected, or none");
            return;
        }
        if (indexes.length==0)
            indexes = getAllIndexes();
        ShapeRoi s1=null, s2=null;
        for (int i=0; i<indexes.length; i++) {
            Roi roi = (Roi)rois.get(indexes[i]);
            if (roi==null || !roi.isArea())
                continue;
            if (s1==null) {
                if (roi instanceof ShapeRoi)
                    s1 = (ShapeRoi)roi.clone();
                else
                    s1 = new ShapeRoi(roi);
                if (s1==null) return;
            } else {
                if (roi instanceof ShapeRoi)
                    s2 = (ShapeRoi)roi.clone();
                else
                    s2 = new ShapeRoi(roi);
                if (s2==null) continue;
                s1.and(s2);
            }
        }
        if (s1!=null) imp.setRoi(s1);
        if (record()) Recorder.record("roiManager", "AND");
    }

    void xor() {
        ImagePlus imp = getImage();
        if (imp==null) return;
        int[] indexes = getSelectedIndexes();
        if (indexes.length==1) {
            error("More than one item must be selected, or none");
            return;
        }
        if (indexes.length==0)
            indexes = getAllIndexes();
        ShapeRoi s1=null, s2=null;
        for (int i=0; i<indexes.length; i++) {
            Roi roi = (Roi)rois.get(indexes[i]);
            if (!roi.isArea()) continue;
            if (s1==null) {
                if (roi instanceof ShapeRoi)
                    s1 = (ShapeRoi)roi.clone();
                else
                    s1 = new ShapeRoi(roi);
                if (s1==null) return;
            } else {
                if (roi instanceof ShapeRoi)
                    s2 = (ShapeRoi)roi.clone();
                else
                    s2 = new ShapeRoi(roi);
                if (s2==null) continue;
                s1.xor(s2);
            }
        }
        if (s1!=null) imp.setRoi(s1);
        if (record()) Recorder.record("roiManager", "XOR");
    }

    void addParticles() {
        String err = IJ.runMacroFile("ij.jar:AddParticles", null);
        if (err!=null && err.length()>0)
            error(err);
    }

    void sort() {
        int n = listModel.size();
        if (n==0)
            return;
        String[] labels = new String[n];
        for (int i=0; i<n; i++)
            labels[i] = (String)listModel.get(i);
        int[] indices = Tools.rank(labels);
        Roi[] rois2 = getRoisAsArray();
        listModel.removeAllElements();
        rois.clear();
        for (int i=0; i<labels.length; i++) {
            listModel.addElement(labels[indices[i]]);
            rois.add(rois2[indices[i]]);
        }
        if (record()) Recorder.record("roiManager", "Sort");
    }
    
    void specify() {
        try {IJ.run("Specify...");}
        catch (Exception e) {return;}
        runCommand("add");
    }
    
    private static boolean channel=false, slice=true, frame=false;
    
    private void removePositions(int position) {
        int[] indexes = getIndexes();
        if (indexes.length==0)
            return;
        boolean removeChannels = position==CHANNEL;
        boolean removeFrames = position==FRAME;
        boolean removeSlices = !(removeChannels||removeFrames);
        ImagePlus imp = WindowManager.getCurrentImage();
        if (position==SHOW_DIALOG) {
            if (imp!=null && !imp.isHyperStack()) {
                channel=false; slice=true; frame=false;
            }
            if (imp!=null && imp.isHyperStack()) {
                channel = slice = frame = false;
                if (imp.getNSlices()>1)
                    slice = true;
                if (imp.getNFrames()>1 && imp.getNSlices()==1)
                    frame = true;
            }
            Font font = new Font("SansSerif", Font.BOLD, 12);
            GenericDialog gd = new GenericDialog("Remove");
            gd.setInsets(5,15,0);
            gd.addMessage("Remove positions for:      ", font);
            gd.setInsets(6,25,0);
            gd.addCheckbox("Channels:", channel);
            gd.setInsets(0,25,0);
            gd.addCheckbox("Slices:", slice);
            gd.setInsets(0,25,0);
            gd.addCheckbox("Frames:", frame);
            gd.showDialog();
            if (gd.wasCanceled())
                return;
            removeChannels = gd.getNextBoolean();
            removeSlices = gd.getNextBoolean();
            removeFrames = gd.getNextBoolean();
        }
        if (!removeChannels && !removeSlices && !removeFrames) {
            slice = true;
            return;
        }
        for (int i=0; i<indexes.length; i++) {
            int index = indexes[i];
            Roi roi = (Roi)rois.get(index);
            String name = (String)listModel.getElementAt(index);
            int n = getSliceNumber(name);
            if (n>0) {
                String name2 = name.substring(5, name.length());
                roi.setName(name2);
                rois.set(index, roi);
                listModel.setElementAt(name2, index);
            }
            int c = roi.getCPosition();
            int z = roi.getZPosition();
            int t = roi.getTPosition();
            if (c>0 || t>0) {
                if (removeChannels) c = 0;
                if (removeSlices) z = 0;
                if (removeFrames) t = 0;
                roi.setPosition(c, z, t);
            } else
                roi.setPosition(0);
        }
        if (imp!=null)
            imp.draw();
        if (record()) {
            if (removeChannels) Recorder.record("roiManager", "Remove Channel Info");
            if (removeSlices) Recorder.record("roiManager", "Remove Slice Info");
            if (removeFrames) Recorder.record("roiManager", "Remove Frame Info");
        }
    }

    private void help() {
        String macro = "run('URL...', 'url="+IJ.URL+"/docs/menus/analyze.html#manager');";
        new MacroRunner(macro);
    }

    private void labels() {
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp!=null) {
            showAllCheckbox.setState(true);
            labelsCheckbox.setState(true);
            showAll(LABELS);
        }
        try {
            IJ.run("Labels...");
        } catch(Exception e) {}
        Overlay defaultOverlay = OverlayLabels.createOverlay();
        Prefs.useNamesAsLabels = defaultOverlay.getDrawNames();
    }

    private void options() {
        Color c = ImageCanvas.getShowAllColor();
        GenericDialog gd = new GenericDialog("Options");
        //gd.addPanel(makeButtonPanel(gd), GridBagConstraints.CENTER, new Insets(5, 0, 0, 0));
        gd.addCheckbox("Associate \"Show All\" ROIs with slices", Prefs.showAllSliceOnly);
        gd.addCheckbox("Restore ROIs centered", restoreCentered);
        gd.addCheckbox("Use ROI names as labels", Prefs.useNamesAsLabels);
        gd.showDialog();
        if (gd.wasCanceled()) {
            if (c!=ImageCanvas.getShowAllColor())
                ImageCanvas.setShowAllColor(c);
            return;
        }
        Prefs.showAllSliceOnly = gd.getNextBoolean();
        restoreCentered = gd.getNextBoolean();
        Prefs.useNamesAsLabels = gd.getNextBoolean();
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp!=null) {
            Overlay overlay = imp.getOverlay();
            if (overlay==null) {
                ImageCanvas ic = imp.getCanvas();
                if (ic!=null)
                    overlay = ic.getShowAllList();
            }
            if (overlay!=null) {
                overlay.drawNames(Prefs.useNamesAsLabels);
                setOverlay(imp, overlay);
            } else
                imp.draw();
        }
        if (record()) {
            Recorder.record("roiManager", "Associate", Prefs.showAllSliceOnly?"true":"false");
            Recorder.record("roiManager", "Centered", restoreCentered?"true":"false");
            Recorder.record("roiManager", "UseNames", Prefs.useNamesAsLabels?"true":"false");
        }
    }

    Panel makeButtonPanel(GenericDialog gd) {
        Panel panel = new Panel();
        //buttons.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 0));
        colorButton = new Button("\"Show All\" Color...");
        colorButton.addActionListener(this);
        panel.add(colorButton);
        return panel;
    }
    
    void setShowAllColor() {
            ColorChooser cc = new ColorChooser("\"Show All\" Color", ImageCanvas.getShowAllColor(),  false);
            ImageCanvas.setShowAllColor(cc.getColor());
    }

    void split() {
        ImagePlus imp = getImage();
        if (imp==null) return;
        Roi roi = imp.getRoi();
        if (roi==null || roi.getType()!=Roi.COMPOSITE) {
            error("Image with composite selection required");
            return;
        }
        boolean record = Recorder.record;
        Recorder.record = false;
        Roi[] rois = ((ShapeRoi)roi).getRois();
        for (int i=0; i<rois.length; i++) {
            imp.setRoi(rois[i]);
            addRoi(false);
        }
        Recorder.record = record;
        if (record()) Recorder.record("roiManager", "Split");
    }
    
    void showAll(int mode) {
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp==null)
            return;
        boolean showAll = mode==SHOW_ALL;
        if (showAll)
            imageID = imp.getID();
        if (mode==LABELS || mode==NO_LABELS)
            showAll = true;
        if (showAll) imp.deleteRoi();
        if (mode==SHOW_NONE) {
            removeOverlay(imp);
            imageID = 0;
        } else if (getCount()>0) {
            Roi[] rois = getRoisAsArray();
            Overlay overlay = newOverlay();
            for (int i=0; i<rois.length; i++)
                overlay.add(rois[i]);
            setOverlay(imp, overlay);
        }
    }

    void updateShowAll() {
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp==null)
            return;
        if (showAllCheckbox.getState()) {
            if (getCount()>0) {
                Roi[] rois = getRoisAsArray();
                Overlay overlay = newOverlay();
                for (int i=0; i<rois.length; i++)
                    overlay.add(rois[i]);
                setOverlay(imp, overlay);
            } else
                removeOverlay(imp);
        } else
            removeOverlay(imp);
    }

    int[] getAllIndexes() {
        int count = getCount();
        int[] indexes = new int[count];
        for (int i=0; i<count; i++)
            indexes[i] = i;
        return indexes;
    }
        
    ImagePlus getImage() {
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp==null) {
            error("There are no images open.");
            return null;
        } else
            return imp;
    }

    boolean error(String msg) {
        new MessageDialog(this, "ROI Manager", msg);
        Macro.abort();
        return false;
    }
    
    public void processWindowEvent(WindowEvent e) {
        super.processWindowEvent(e);
        if (e.getID()==WindowEvent.WINDOW_CLOSING) {
            instance = null;    
        }
        if (!IJ.isMacro())
            ignoreInterrupts = false;
    }
    
    /** Returns a reference to the ROI Manager and opens
         the "ROI Manager" window if it is not already open. */
    public static RoiManager getRoiManager() {
        if (instance!=null)
            return (RoiManager)instance;
        else
            return new RoiManager();
    }

    /** Returns a reference to the ROI Manager, or null if it is not open
     * and a batch mode macro is not running. If the ROI Manager 
     * is not open and a batch mode macro is running, 
     * returns the hidden batch mode RoiManager.
     * @see #getRoiManager
    */
    public static RoiManager getInstance() {
        if (instance==null && IJ.isMacro())
            return Interpreter.getBatchModeRoiManager();
        else
            return (RoiManager)instance;
    }
    
    public static RoiManager getRawInstance() {
        return (RoiManager)instance;
    }

    /** Returns a reference to the ROI Manager window or to the
        macro batch mode RoiManager, or null if neither exists. */
    public static RoiManager getInstance2() {
        RoiManager rm = getInstance();
        if (rm==null && IJ.isMacro())
            rm = Interpreter.getBatchModeRoiManager();
        return rm;
    }

    /** Obsolete
     * @deprecated
     * @see #getCount
     * @see #getRoisAsArray
    */
    public Hashtable getROIs() {
        Roi[] rois = getRoisAsArray();
        Hashtable ht = new Hashtable();
        for (int i=0; i<rois.length; i++)
            ht.put((String)listModel.getElementAt(i), rois[i]);
        return ht;
    }

    /** Obsolete
     * @deprecated
     * @see #getCount
     * @see #getRoisAsArray
     * @see #getSelectedIndex
    */
    public List getList() {
        List awtList = new List();
        for (int i=0; i<getCount(); i++)
            awtList.add((String)listModel.getElementAt(i));
        int index = getSelectedIndex();
        if (index>=0)
            awtList.select(index);
        return awtList;
    }
    
    /** Returns the ROI count. */
    public int getCount() {
        return listModel!=null?listModel.getSize():0;
    }

    /** Returns the index of the specified Roi, or -1 if it is not found. */
    public int getRoiIndex(Roi roi) {
        int n = getCount();
        for (int i=0; i<n; i++) {
            Roi roi2 = (Roi)rois.get(i);
            if (roi==roi2)
                return i;
        }
        return -1;
    }
    
    /** Returns the index of the first selected ROI or -1 if no ROI is selected. */
    public int getSelectedIndex() {
        return list.getSelectedIndex();
    }
    
    /** Returns a reference to the ROI at the specified index. */
    public Roi getRoi(int index) {
        if (index<0 || index>=getCount())
            return null;
        return (Roi)rois.get(index);
    }

    /** Returns the ROIs as an array. */
    public synchronized Roi[] getRoisAsArray() {
        Roi[] array = new Roi[rois.size()];
        return (Roi[])rois.toArray(array);
    }
    
    /** Returns the selected ROIs as an array, or
        all the ROIs if none are selected. */
    public Roi[] getSelectedRoisAsArray() {
        int[] indexes = getIndexes();
        int n = indexes.length;
        Roi[] array = new Roi[n];
        for (int i=0; i<n; i++)
            array[i] = (Roi)rois.get(indexes[i]);
        return array;
    }
            
    /** Returns the name of the ROI with the specified index,
        or null if the index is out of range. */
    public String getName(int index) {
        if (index>=0 && index<getCount())
            return  (String) listModel.getElementAt(index);
        else
            return null;
    }

    /** Returns the name of the ROI with the specified index.
        Can be called from a macro using
        <pre>call("ij.plugin.frame.RoiManager.getName", index)</pre>
        Returns "null" if the Roi Manager is not open or index is
        out of range.
    */
    public static String getName(String index) {
        int i = (int)Tools.parseDouble(index, -1);
        RoiManager instance = getInstance2();
        if (instance!=null && i>=0 && i<instance.getCount())
            return  (String) instance.listModel.getElementAt(i);
        else
            return "null";
    }

    /** Executes the ROI Manager "Add", "Add & Draw", "Update", "Delete", "Measure", "Draw",
        "Show All", "Show None", "Fill", "Deselect", "Select All", "Combine", "AND", "XOR", "Split",
        "Sort" or "Multi Measure" command.  Returns false if <code>cmd</code>
        is not one of these strings. */
    public boolean runCommand(String cmd) {
        cmd = cmd.toLowerCase();
        macro = true;
        boolean ok = true;
        if (cmd.equals("add")) {
            boolean shift = IJ.shiftKeyDown();
            boolean alt = IJ.altKeyDown();
            if (Interpreter.isBatchMode()) {
                shift = false;
                alt = false;
            }
            add(shift, alt);
            if (IJ.isJava18()&&IJ.isMacOSX())
                repaint();
        } else if (cmd.equals("add & draw"))
            addAndDraw(false);
        else if (cmd.equals("update"))
            update(true);
        else if (cmd.equals("update2"))
            update(false);
        else if (cmd.equals("delete"))
            delete(false);
        else if (cmd.equals("measure"))
            measure(COMMAND);
        else if (cmd.equals("draw"))
            drawOrFill(DRAW);
        else if (cmd.equals("fill"))
            drawOrFill(FILL);
        else if (cmd.equals("label"))
            drawOrFill(LABEL);
        else if (cmd.equals("and"))
            and();
        else if (cmd.equals("or") || cmd.equals("combine"))
            combine();
        else if (cmd.equals("xor"))
            xor();
        else if (cmd.equals("split"))
            split();
        else if (cmd.equals("sort"))
            sort();
        else if (cmd.startsWith("multi measure") || cmd.startsWith("multi-measure"))
            multiMeasure(cmd);
        else if (cmd.equals("multi plot"))
            multiPlot();
        else if (cmd.equals("show all")) {
            if (WindowManager.getCurrentImage()!=null) {
                showAll(SHOW_ALL);
                showAllCheckbox.setState(true);
            }
        } else if (cmd.equals("show none")) {
            if (WindowManager.getCurrentImage()!=null) {
                showAll(SHOW_NONE);
                showAllCheckbox.setState(false);
            }
        } else if (cmd.equals("show all with labels")) {
            labelsCheckbox.setState(true);
            showAll(LABELS);
            showAllCheckbox.setState(true);
            if (Interpreter.isBatchMode()) IJ.wait(250);
        } else if (cmd.equals("show all without labels")) {
            showAllCheckbox.setState(true);
            labelsCheckbox.setState(false);
            showAll(NO_LABELS);
            if (Interpreter.isBatchMode()) IJ.wait(250);
        } else if (cmd.equals("deselect")||cmd.indexOf("all")!=-1) {
            if (IJ.isMacOSX()) ignoreInterrupts = true;
            deselect();
            IJ.wait(50);
        } else if (cmd.equals("reset")) {
            reset();
        } else if (cmd.equals("debug")) {
            //IJ.log("Debug: "+debugCount);
            //for (int i=0; i<debugCount; i++)
            //  IJ.log(debug[i]);
        } else if (cmd.equals("enable interrupts")) {
            ignoreInterrupts = false;
        } else if (cmd.equals("remove channel info")) {
            removePositions(CHANNEL);
        } else if (cmd.equals("remove slice info")) {
            removePositions(SLICE);
        } else if (cmd.equals("remove frame info")) {
            removePositions(FRAME);
        } else if (cmd.equals("list")) {
            listRois();
        } else if (cmd.equals("interpolate rois")) {
            interpolateRois();
        } else
            ok = false;
        macro = false;
        return ok;
    }

    /** Using the specified image, runs the ROI Manager "Add", "Add & Draw", "Update",
        "Delete", "Measure", "Draw", "Show All", "Show None", "Fill", "Deselect", "Select All", 
        "Combine", "AND", "XOR", "Split", "Sort" or "Multi Measure" command. */
    public boolean runCommand(ImagePlus imp, String cmd) {
        WindowManager.setTempCurrentImage(imp);
        boolean ok = runCommand(cmd);
        WindowManager.setTempCurrentImage(null);
        return ok;
    }

    /** Executes the ROI Manager "Open", "Save" or "Rename" command. Returns false if 
    <code>cmd</code> is not "Open", "Save" or "Rename", or if an error occurs. */
    public boolean runCommand(String cmd, String name) {
        cmd = cmd.toLowerCase();
        macro = true;
        if (cmd.equals("open")) {
            open(name);
            macro = false;
            return true;
        } else if (cmd.equals("save")) {
            save(name, false);
        } else if (cmd.equals("save selected")) {
            save(name, true);
        } else if (cmd.equals("rename")) {
            rename(name);
            macro = false;
            return true;
        } else if (cmd.equals("set color")) {
            Color color = Colors.decode(name, Color.cyan);
            setProperties(color, -1, null);
            macro = false;
            return true;
        } else if (cmd.equals("set fill color")) {
            Color fillColor = Colors.decode(name, Color.cyan);
            setProperties(null, -1, fillColor);
            macro = false;
            return true;
        } else if (cmd.equals("set line width")) {
            int lineWidth = (int)Tools.parseDouble(name, 0);
            if (lineWidth>=0)
                setProperties(null, lineWidth, null);
            macro = false;
            return true;
        } else if (cmd.equals("associate")) {
            Prefs.showAllSliceOnly = name.equals("true")?true:false;
            macro = false;
            return true;
        } else if (cmd.equals("centered")) {
            restoreCentered = name.equals("true")?true:false;
            macro = false;
            return true;
        } else if (cmd.equals("usenames")) {
            Prefs.useNamesAsLabels = name.equals("true")?true:false;
            macro = false;
            if (labelsCheckbox.getState()) {
                ImagePlus imp = WindowManager.getCurrentImage();
                if (imp!=null) imp.draw();
            }
            return true;
        }
        return false;
    }
    
    /** Clears this RoiManager so that it contains no ROIs. */
    public void reset() {
        if (IJ.isMacOSX() && IJ.isMacro())
            ignoreInterrupts = true;
        listModel.removeAllElements();
        rois.clear();
        updateShowAll();
    }
    
    private void translate() {
        double dx = 10.0;
        double dy = 10.0;
        GenericDialog gd = new GenericDialog("Translate");
        gd.addNumericField("X offset (pixels): ", dx, 0);
        gd.addNumericField("Y offset (pixels): ", dy, 0);
        gd.showDialog();
        if (gd.wasCanceled())
            return;
        dx = gd.getNextNumber();
        dy = gd.getNextNumber();
        translate(dx, dy);
        if (record()) {
            if (Recorder.scriptMode())
                Recorder.recordCall("rm.translate("+dx+", "+dy+");");
            else
                Recorder.record("roiManager", "translate", (int)dx, (int)dy);
        }
    }

    /** Moves the selected ROIs or all the ROIs if none are selected. */
    public void translate(double dx, double dy) {
        Roi[] rois = getSelectedRoisAsArray();
        for (int i=0; i<rois.length; i++) {
            Roi roi = rois[i];
            Rectangle2D r = roi.getFloatBounds();
            roi.setLocation(r.getX()+dx, r.getY()+dy);
        }
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp!=null) {
            Roi roi = imp.getRoi();
            if (roi!=null && !(rois.length==1 && rois[0]==roi)) {
                Rectangle2D r = roi.getFloatBounds();
                roi.setLocation(r.getX()+dx, r.getY()+dy);
            }
            imp.draw();
        }
    }

    private boolean save(String name, boolean saveSelected) {
        if (!name.endsWith(".zip") && !name.equals(""))
            return error("Name must end with '.zip'");
        if (getCount()==0)
            return error("The selection list is empty.");
        int[] indexes = null;
        if (saveSelected)
            indexes = getIndexes();
        else
            indexes = getAllIndexes();
        boolean ok = false;
        if (name.equals(""))
            ok = saveMultiple(indexes, null);
        else
            ok = saveMultiple(indexes, name);
        macro = false;
        return ok;
    }
    
    /** Adds the current selection to the ROI Manager, using the
        specified color (a 6 digit hex string) and line width. */
    public boolean runCommand(String cmd, String hexColor, double lineWidth) {
        if (hexColor==null && lineWidth==1.0 && (IJ.altKeyDown()&&!Interpreter.isBatchMode()))
            addRoi(true);
        else {
            Color color = hexColor!=null?Colors.decode(hexColor, Color.cyan):null;
            addRoi(null, false, color, (int)Math.round(lineWidth));
        }
        return true;    
    }
        
    /** Assigns the ROI at the specified index to the current image. */
    public void select(int index) {
        select(null, index);
    }
    
    /** Assigns the ROI at the specified index to 'imp'. */
    public void select(ImagePlus imp, int index) {
        selectedIndexes = null;
        if (index<0) {
            deselect();
            return;
        }
        int n = getCount();
        if (index>=n) return;
        boolean mm = list.getSelectionMode() == ListSelectionModel.MULTIPLE_INTERVAL_SELECTION;
        if (mm) list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        int delay = 1;
        long start = System.currentTimeMillis();
        while (true) {
            if (list.isSelectedIndex(index))
                break;
            list.clearSelection();
            list.setSelectedIndex(index);
        }
        if (imp==null)
            imp = WindowManager.getCurrentImage();
        if (imp!=null)
            restore(imp, index, true);
        if (mm) list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
    }
    
    public void selectAndMakeVisible(ImagePlus imp, int index) {
        select(imp, index);
        list.ensureIndexIsVisible(index);
    }
    
    public void select(int index, boolean shiftKeyDown, boolean altKeyDown) {
        if (!(shiftKeyDown||altKeyDown))
            select(index);
        ImagePlus imp = IJ.getImage();
        if (imp==null)
            return;
        Roi previousRoi = imp.getRoi();
        if (previousRoi==null) {
            select(index);
            return;
        }
        Roi.previousRoi = (Roi)previousRoi.clone();
        Roi roi = (Roi)rois.get(index);
        if (roi!=null) {
            roi.setImage(imp);
            roi.update(shiftKeyDown, altKeyDown);
        }
    }
    
    public void deselect() {
        int n = getCount();
        for (int i=0; i<n; i++)
            list.clearSelection();
        if (record()) Recorder.record("roiManager", "Deselect");
        return;
    }
    
    /** Deselect the specified ROI if it is the only one selected. */
    public void deselect(Roi roi) {
        int[] indexes = getSelectedIndexes();
        if (indexes.length==1) {
            String label = (String)listModel.getElementAt(indexes[0]);
            if (label.equals(roi.getName())) {
                deselect();
                repaint();
            }
        }
    }

    public void setEditMode(ImagePlus imp, boolean editMode) {
        showAllCheckbox.setState(editMode);
        labelsCheckbox.setState(editMode);
        showAll(editMode?LABELS:SHOW_NONE);
    }
    
    /** Overrides PlugInFrame.close(). */
    public void close() {
        super.close();
        instance = null;
        resetMultiMeasureResults();
        Prefs.saveLocation(LOC_KEY, getLocation());
        if (!showAllCheckbox.getState() || IJ.macroRunning())
            return;
        int n = getCount();
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp==null || (imp.getCanvas()!=null && imp.getCanvas().getShowAllList()==null))
            return;
        if (n>0) {
            GenericDialog gd = new GenericDialog("ROI Manager");
            gd.addMessage("Save the "+n+" displayed ROIs as an overlay?");
            gd.setOKLabel("Discard");
            gd.setCancelLabel("Save as Overlay");
            gd.showDialog();
            if (gd.wasCanceled())
                moveRoisToOverlay(imp);
            else
                removeOverlay(imp);
        } else
            imp.draw();
    }
    
    /** Moves all the ROIs to the specified image's overlay. */
    public void moveRoisToOverlay(ImagePlus imp) {
        if (imp==null)
            return;
        Roi[] rois = getRoisAsArray();
        int n = rois.length;
        Overlay overlay = imp.getOverlay();
        if (overlay==null)
            overlay = newOverlay();
        for (int i=0; i<n; i++) {
            Roi roi = (Roi)rois[i].clone();
            if (!Prefs.showAllSliceOnly && !IJ.isMacro())
                roi.setPosition(0);
            if (roi.getStrokeWidth()==1)
                roi.setStrokeWidth(0);
            overlay.add(roi);
        }
        imp.setOverlay(overlay);
        if (imp.getCanvas()!=null)
            setOverlay(imp, null);
    }
    
    public void mousePressed (MouseEvent e) {
        int x=e.getX(), y=e.getY();
        if (e.isPopupTrigger() || e.isMetaDown())
            pm.show(e.getComponent(),x,y);
    }

    public void mouseWheelMoved(MouseWheelEvent event) {
        synchronized(this) {
            int index = list.getSelectedIndex();
            int rot = event.getWheelRotation();
            if (rot<-1) rot = -1;
            if (rot>1) rot = 1;
            index += rot;
            if (index<0) index = 0;
            if (index>=getCount()) index = getCount();
            //IJ.log(index+"  "+rot);
            select(index);
            if (IJ.isWindows())
                list.requestFocusInWindow();
            if (IJ.isJava18()&&IJ.isMacOSX())
                repaint();
        }
    }
    
    /** Selects multiple ROIs, where 'indexes' is an array of integers, 
        each greater than or equal to 0 and less than the value returned by getCount().
    */
    /** Selects multiple ROIs, where 'indexes' is an array of integers, each
    * greater than or equal to 0 and less than the value returned by getCount().
    * @see #getSelectedIndexes
    * @see #getSelectedRoisAsArray
    * @see #getCount
    */
    public void setSelectedIndexes(int[] indexes) {
        int count = getCount();
        if (count==0) return;
        for (int i=0; i<indexes.length; i++) {
            if (indexes[i]<0) indexes[i]=0;
            if (indexes[i]>=count) indexes[i]=count-1;
        }
        selectedIndexes = indexes;
        list.setSelectedIndices(indexes);
    }
    
    /** Returns an array of the selected indexes. */
    public int[] getSelectedIndexes() {
        if (selectedIndexes!=null) {
            int[] indexes = selectedIndexes;
            selectedIndexes = null;
            return indexes;
        } else
            return list.getSelectedIndices();
    }
    
    /** Returns an array of the selected indexes or all indexes if none are selected. */
    public int[] getIndexes() {
        int[] indexes = getSelectedIndexes();
        if (indexes.length==0)
            indexes = getAllIndexes();
        return indexes;
    }
    
    /** Returns 'true' if the index is valid and the indexed ROI is selected. */
    public boolean isSelected(int index) {
        return index>=0 && index<listModel.getSize() && list.isSelectedIndex(index);
    }
    
    private Overlay newOverlay() {
        Overlay overlay = OverlayLabels.createOverlay();
        overlay.drawLabels(labelsCheckbox.getState());
        if (overlay.getLabelFont()==null && overlay.getLabelColor()==null) {
            overlay.setLabelColor(Color.white);
            overlay.drawBackgrounds(true);
        }
        overlay.drawNames(Prefs.useNamesAsLabels);
        return overlay;
    }

    private void removeOverlay(ImagePlus imp) {
        if (imp!=null && imp.getCanvas()!=null)
            setOverlay(imp, null);
    }
    
    private void setOverlay(ImagePlus imp, Overlay overlay) {
        if (imp==null)
            return;
        ImageCanvas ic = imp.getCanvas();
        if (ic==null) {
            imp.setOverlay(overlay);
            return;
        }
        ic.setShowAllList(overlay);
        imp.draw();
    }
    
    private boolean record() {
        return Recorder.record && allowRecording && !IJ.isMacro();
    }
    
    private boolean recordInEvent() {
        return Recorder.record && !IJ.isMacro();
    }

    public void allowRecording(boolean allow) {
        this.allowRecording = allow;
    }
    
    public void mouseReleased (MouseEvent e) {}
    public void mouseClicked (MouseEvent e) {}
    public void mouseEntered (MouseEvent e) {}
    public void mouseExited (MouseEvent e) {}
    
    public void valueChanged(ListSelectionEvent e) {
        if (e.getValueIsAdjusting())
            return;
        if (getCount()==0) {
            if (recordInEvent())
                Recorder.record("roiManager", "Deselect");
            return;
        }
        int[] selected = list.getSelectedIndices();
        if (selected.length==0) {
            imageID = 0;
            return;
        }
        if (WindowManager.getCurrentImage()!=null) {
            if (selected.length==1) {
                ImagePlus imp = getImage();
                if (imp!=null) {
                    Roi roi = imp.getRoi();
                    if (roi!=null)
                        Roi.previousRoi = (Roi)roi.clone();
                }
                restore(imp, selected[0], true);
                imageID = imp!=null?imp.getID():0;
            }
            if (recordInEvent()) {
                String arg = Arrays.toString(selected);
                if (!arg.startsWith("[") || !arg.endsWith("]"))
                    return;
                arg = arg.substring(1, arg.length()-1);
                arg = arg.replace(" ", "");
                if (Recorder.scriptMode()) {
                    if (selected.length==1)
                        Recorder.recordCall("rm.select("+arg+");");
                    else
                        Recorder.recordCall("rm.setSelectedIndexes(["+arg+"]);");
                } else {
                    if (selected.length == 1)
                        Recorder.recordString("roiManager(\"Select\", " + arg + ");\n");
                    else
                        Recorder.recordString("roiManager(\"Select\", newArray(" + arg + "));\n");
                }
            }
        }
    }

    public void windowActivated(WindowEvent e) {
        super.windowActivated(e);
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp!=null) {
            if (imageID!=0 && imp.getID()!=imageID) {
                showAll(SHOW_NONE);
                if (okToSet())
                    showAllCheckbox.setState(false);
                deselect();
                imageID = 0;
            }
        }
    }
    
    public static void resetMultiMeasureResults() {
        mmResults = mmResults2 = null;
    }
    
    // This class runs the "Multi Measure" command in a separate thread
    private class MultiMeasureRunner implements Runnable  {
        private Thread thread;
        private ImagePlus imp;
        private Roi[] rois;
        private boolean appendResults;
        
        public void multiMeasure(ImagePlus imp, Roi[] rois, boolean appendResults) {
            this.imp = imp;
            this.rois = rois;
            this.appendResults = appendResults;
            thread = new Thread(this, "MultiMeasure"); 
            thread.start();
        }
    
        public void run() {
            int currentSlice = imp.getCurrentSlice();
            ResultsTable rtMulti = RoiManager.multiMeasure(imp, rois, appendResults);
            mmResults = (ResultsTable)rtMulti.clone();
            rtMulti.show("Results");
            imp.setSlice(currentSlice);
            if (rois.length>1)
                IJ.run("Select None");
        }
        
    }


}