package ij.plugin;
import ij.*;
import ij.process.*;
import ij.gui.*;
import java.awt.*;
import ij.measure.*;
import java.awt.event.*;

/** This plugin implements the Analyze/Tools/Scale Bar command.
 * Divakar Ramachandran added options to draw a background 
 * and use a serif font on 23 April 2006.
 * Remi Berthoz added an option to draw vertical scale
 * bars on 17 September 2021.
*/
public class ScaleBar implements PlugIn {

    static final String[] locations = {"Upper Right", "Lower Right", "Lower Left", "Upper Left", "At Selection"};
    static final int UPPER_RIGHT=0, LOWER_RIGHT=1, LOWER_LEFT=2, UPPER_LEFT=3, AT_SELECTION=4;
    static final String[] colors = {"White","Black","Light Gray","Gray","Dark Gray","Red","Green","Blue","Yellow"};
    static final String[] bcolors = {"None","Black","White","Dark Gray","Gray","Light Gray","Yellow","Blue","Green","Red"};
    static final String[] checkboxLabels = {"Horizontal", "Vertical", "Bold Text", "Hide Text", "Serif Font", "Overlay"};
    final static String SCALE_BAR = "|SB|";
    
    private static final ScaleBarConfiguration sConfig = new ScaleBarConfiguration();
    private ScaleBarConfiguration config = new ScaleBarConfiguration(sConfig);

    ImagePlus imp;
    int hBarWidthInPixels;
    int vBarHeightInPixels;
    int roiX, roiY, roiWidth, roiHeight;
    boolean userRoiExists;
    boolean[] checkboxStates = new boolean[6];

    Rectangle hBackground = new Rectangle();
    Rectangle hBar = new Rectangle();
    Rectangle hText = new Rectangle();
    Rectangle vBackground = new Rectangle();
    Rectangle vBar = new Rectangle();
    Rectangle vText = new Rectangle();

    /**
     * This method is called when the plugin is loaded. 'arg', which
     * may be blank, is the argument specified for this plugin in
     * IJ_Props.txt.
     */
    public void run(String arg) {
        imp = WindowManager.getCurrentImage();
        if (imp == null) {
            IJ.noImage();
            return;
        }
        // Snapshot before anything, so we can revert if the user cancels the action.
        imp.getProcessor().snapshot();

        userRoiExists = parseCurrentROI();
        boolean userOKed = askUserConfiguration(userRoiExists);

        if (!userOKed) {
            removeScalebar();
            return;
        }

        if (!IJ.isMacro())
            persistConfiguration();
        updateScalebar(!config.labelAll);
     }

    /**
     * Remove the scalebar drawn by this plugin.
     * 
     * If the scalebar was drawn without the overlay by another
     * instance of the plugin (it is drawn into the image), then
     * we cannot remove it.
     * 
     * If the scalebar was drawn using the overlay by another
     * instance of the plugin, then we can remove it.
     * 
     * With or without the overlay, we can remove a scalebar
     * drawn by this instance of the plugin.
     */
    void removeScalebar() {
        // Revert with Undo, in case "Use Overlay" is not ticked
        imp.getProcessor().reset();
        imp.updateAndDraw();
        // Remove overlay drawn by this plugin, in case "Use Overlay" is ticked
        Overlay overlay = imp.getOverlay();
        if (overlay!=null) {
            overlay.remove(SCALE_BAR);
            imp.draw();
        }
    }

    /**
     * If there is a user selected ROI, set the class variables {roiX}
     * and {roiY}, {roiWidth}, {roiHeight} to the corresponding
     * features of the ROI, and return true. Otherwise, return false.
     */
    boolean parseCurrentROI() {
        Roi roi = imp.getRoi();
        if (roi == null) return false;

        Rectangle r = roi.getBounds();
        roiX = r.x;
        roiY = r.y;
        roiWidth = r.width;
        roiHeight = r.height;
        return true;
    }

    /**
     * There is no hard codded value for the width of the scalebar,
     * when the plugin is called for the first time in an ImageJ
     * instance, a defautl value for the width will be computed by
     * this method.
     */
    void computeDefaultBarWidth(boolean currentROIExists) {
        Calibration cal = imp.getCalibration();
        ImageWindow win = imp.getWindow();
        double mag = (win!=null)?win.getCanvas().getMagnification():1.0;
        if (mag>1.0)
            mag = 1.0;

        double pixelWidth = cal.pixelWidth;
        if (pixelWidth==0.0)
            pixelWidth = 1.0;
        double pixelHeight = cal.pixelHeight;
        if (pixelHeight==0.0)
            pixelHeight = 1.0;
        double imageWidth = imp.getWidth()*pixelWidth;
        double imageHeight = imp.getHeight()*pixelHeight;

        if (currentROIExists && roiX>=0 && roiWidth>10) {
            // If the user has a ROI, set the bar width according to ROI width.
            config.hBarWidth = roiWidth*pixelWidth;
        }
        else if (config.hBarWidth<=0.0 || config.hBarWidth>0.67*imageWidth) {
            // If the bar is of negative width or too wide for the image,
            // set the bar width to 80 pixels.
            config.hBarWidth = (80.0*pixelWidth)/mag;
            if (config.hBarWidth>0.67*imageWidth)
                // If 80 pixels is too much, do 2/3 of the image.
                config.hBarWidth = 0.67*imageWidth;
            if (config.hBarWidth>5.0)
                // If the resulting size is larger than 5 units, round the value.
                config.hBarWidth = (int) config.hBarWidth;
        }

        if (currentROIExists && roiY>=0 && roiHeight>10) {
            config.vBarHeight = roiHeight*pixelHeight;
        }
        else if (config.vBarHeight<=0.0 || config.vBarHeight>0.67*imageHeight) {
            config.vBarHeight = (80.0*pixelHeight)/mag;
            if (config.vBarHeight>0.67*imageHeight)
                // If 80 pixels is too much, do 2/3 of the image.
                config.vBarHeight = 0.67*imageHeight;
            if (config.vBarHeight>5.0)
                // If the resulting size is larger than 5 units, round the value.
                config.vBarHeight = (int) config.vBarHeight;
        }
    } 

    /**
     * Genreate & draw the configuration dialog.
     * 
     * Return the value of dialog.wasOKed() when the user clicks OK
     * or Cancel.
     */
    boolean askUserConfiguration(boolean currentROIExists) {
        // Update the user configuration if there is an ROI, or if
        // the defined bar width is negative (it is if it has never
        // been set in this ImageJ instance).
        if (currentROIExists) {
            config.location = locations[AT_SELECTION];
        }
        if (IJ.isMacro())
            config.updateFrom(new ScaleBarConfiguration());
        if (config.hBarWidth <= 0 || config.vBarHeight <= 0 || currentROIExists) {
            computeDefaultBarWidth(currentROIExists);
        }

        // Draw a first preview scalebar, with the default or presisted
        // configuration.
        updateScalebar(true);
        
        // Create & show the dialog, then return.
        boolean multipleSlices = imp.getStackSize() > 1;
        GenericDialog dialog = new BarDialog(getHUnit(), getVUnit(), config.hDigits, config.vDigits, multipleSlices);
        DialogListener dialogListener = new BarDialogListener(multipleSlices);
        dialog.addDialogListener(dialogListener);
        dialog.showDialog();
        return dialog.wasOKed();
    }

    /**
     * Store the active configuration into the static variable that
     * is persisted across calls of the plugin.
     * 
     * The "active" configuration is normally the one reflected by
     * the dialog.
     */
    void persistConfiguration() {
        sConfig.updateFrom(config);
    }
    
    /**
     * Return the X unit strings defined in the image calibration.
     */
    String getHUnit() {
        String hUnits = imp.getCalibration().getXUnits();
        if (hUnits.equals("microns"))
            hUnits = IJ.micronSymbol+"m";
        return hUnits;
    }

    /**
     * Return the Y unit strings defined in the image calibration.
     */
    String getVUnit() {
        String vUnits = imp.getCalibration().getYUnits();
        if (vUnits.equals("microns"))
            vUnits = IJ.micronSymbol+"m";
        return vUnits;
    }

    /**
     * Create & draw the scalebar using an Overlay.
     */
    Overlay createScaleBarOverlay() throws MissingRoiException {
        Overlay overlay = new Overlay();

        Color color = getColor();
        Color bcolor = getBColor();
        
        int fontType = config.boldText?Font.BOLD:Font.PLAIN;
        String face = config.serifFont?"Serif":"SanSerif";
        Font font = new Font(face, fontType, config.fontSize);
        ImageProcessor ip = imp.getProcessor();
        ip.setFont(font);

        setElementsPositions(ip);

        if (bcolor != null) {
            if (config.showHorizontal) {
                Roi hBackgroundRoi = new Roi(hBackground.x, hBackground.y, hBackground.width, hBackground.height);
                hBackgroundRoi.setFillColor(bcolor);
                overlay.add(hBackgroundRoi, SCALE_BAR);
            }
            if (config.showVertical) {
                Roi vBackgroundRoi = new Roi(vBackground.x, vBackground.y, vBackground.width, vBackground.height);
                vBackgroundRoi.setFillColor(bcolor);
                overlay.add(vBackgroundRoi, SCALE_BAR);
            }
        }

        if (config.showHorizontal) {
            Roi hBarRoi = new Roi(hBar.x, hBar.y, hBar.width, hBar.height);
            hBarRoi.setFillColor(color);
            overlay.add(hBarRoi, SCALE_BAR);
        }
        if (config.showVertical) {
            Roi vBarRoi = new Roi(vBar.x, vBar.y, vBar.width, vBar.height);
            vBarRoi.setFillColor(color);
            overlay.add(vBarRoi, SCALE_BAR);
        }

        if (!config.hideText) {
            if (config.showHorizontal) {
                TextRoi hTextRoi = new TextRoi(hText.x, hText.y, getHLabel(), font);
                hTextRoi.setStrokeColor(color);
                overlay.add(hTextRoi, SCALE_BAR);
            }
            if (config.showVertical) {
                TextRoi vTextRoi = new TextRoi(vText.x, vText.y + vText.height, getVLabel(), font);
                vTextRoi.setStrokeColor(color);
                vTextRoi.setAngle(90.0);
                overlay.add(vTextRoi, SCALE_BAR);
            }
        }

        return overlay;
    }

    /**
     * Returns the text to draw near the scalebar (<width> <unit>).
     */
    String getHLabel() {
        return IJ.d2s(config.hBarWidth, config.hDigits) + " " + getHUnit();
    }

    /**
     * Returns the text to draw near the scalebar (<height> <unit>).
     */
    String getVLabel() {
        return IJ.d2s(config.vBarHeight, config.vDigits) + " " + getVUnit();
    }

    /**
     * Returns the width of the box that contains the horizontal scalebar and
     * its label.
     */
    int getHBoxWidthInPixels() {
        updateFont();
        ImageProcessor ip = imp.getProcessor();
        int hLabelWidth = config.hideText ? 0 : ip.getStringWidth(getHLabel());
        int hBoxWidth = Math.max(hBarWidthInPixels, hLabelWidth);
        return (config.showHorizontal ? hBoxWidth : 0);
    }

    /**
     * Returns the height of the box that contains the horizontal scalebar and
     * its label.
     */
    int getHBoxHeightInPixels() {
        int hLabelHeight = config.hideText ? 0 : config.fontSize;
        int hBoxHeight = config.barThicknessInPixels + (int) (hLabelHeight * 1.25);
        return (config.showHorizontal ? hBoxHeight : 0);
    }

    /**
     * Returns the height of the box that contains the vertical scalebar and
     * its label.
     */
    int getVBoxHeightInPixels() {
        updateFont();
        ImageProcessor ip = imp.getProcessor();
        int vLabelHeight = config.hideText ? 0 : ip.getStringWidth(getVLabel());
        int vBoxHeight = Math.max(vBarHeightInPixels, vLabelHeight);
        return (config.showVertical ? vBoxHeight : 0);
    }

    /**
     * Returns the width of the box that contains the vertical scalebar and
     * its label.
     */
    int getVBoxWidthInPixels() {
        int vLabelWidth = config.hideText ? 0 : config.fontSize;
        int vBoxWidth = config.barThicknessInPixels + (int) (vLabelWidth * 1.25);
        return (config.showVertical ? vBoxWidth : 0);
    }

    /**
     * Returns the size of margins that should be displayed between the scalebar
     * elements and the image edge.
     */
    int getOuterMarginSizeInPixels() {
        int imageWidth = imp.getWidth();
        int imageHeight = imp.getHeight();
        return (imageWidth + imageHeight) / 100;
    }

    /**
     * Retruns the size of margins that should be displayed between the scalebar
     * elements and the edge of the element's backround.
     */
    int getInnerMarginSizeInPixels() {
        int maxWidth = Math.max(getHBoxWidthInPixels(), getVBoxHeightInPixels());
        int margin = Math.max(maxWidth/20, 2);
        return config.bcolor.equals("None") ? 0 : margin;
    }

    void updateFont() {
        int fontType = config.boldText?Font.BOLD:Font.PLAIN;
        String font = config.serifFont?"Serif":"SanSerif";
        ImageProcessor ip = imp.getProcessor();
        ip.setFont(new Font(font, fontType, config.fontSize));
        ip.setAntialiasedText(true);
    }

    /**
     * Sets the positions x y of hBackground and vBackground based on
     * the current configuration.
     */
    void setBackgroundBoxesPositions(ImageProcessor ip) throws MissingRoiException {
        Calibration cal = imp.getCalibration();
        hBarWidthInPixels = (int)Math.round(config.hBarWidth/cal.pixelWidth);
        vBarHeightInPixels = (int)Math.round(config.vBarHeight/cal.pixelHeight);

        boolean hTextTop = config.showVertical && (config.location.equals(locations[UPPER_LEFT]) || config.location.equals(locations[UPPER_RIGHT]));
        
        int imageWidth = imp.getWidth();
        int imageHeight = imp.getHeight();
        int hBoxWidth = getHBoxWidthInPixels();
        int hBoxHeight = getHBoxHeightInPixels();
        int vBoxWidth = getVBoxWidthInPixels();
        int vBoxHeight = getVBoxHeightInPixels();
        int outerMargin = getOuterMarginSizeInPixels();
        int innerMargin = getInnerMarginSizeInPixels();
        
        hBackground.width = innerMargin + hBoxWidth + innerMargin;
        hBackground.height = innerMargin + hBoxHeight + innerMargin;
        vBackground.width = innerMargin + vBoxWidth + innerMargin;
        vBackground.height = innerMargin + vBoxHeight + innerMargin;

        if (config.location.equals(locations[UPPER_RIGHT])) {
            hBackground.x = imageWidth - outerMargin - innerMargin - vBoxWidth + (config.showVertical ? config.barThicknessInPixels : 0) - hBoxWidth - innerMargin;
            hBackground.y = outerMargin;
            vBackground.x = imageWidth - outerMargin - innerMargin - vBoxWidth - innerMargin;
            vBackground.y = outerMargin + (hTextTop ? hBoxHeight - config.barThicknessInPixels : 0);
            hBackground.width += (config.showVertical ? vBoxWidth - config.barThicknessInPixels : 0);

        } else if (config.location.equals(locations[LOWER_RIGHT])) {
            hBackground.x = imageWidth - outerMargin - innerMargin - vBoxWidth - hBoxWidth + (config.showVertical ? config.barThicknessInPixels : 0) - innerMargin;
            hBackground.y = imageHeight - outerMargin - innerMargin - hBoxHeight - innerMargin;
            vBackground.x = imageWidth - outerMargin - innerMargin - vBoxWidth - innerMargin;
            vBackground.y = imageHeight - outerMargin - innerMargin - hBoxHeight + (config.showHorizontal ? config.barThicknessInPixels : 0) - vBoxHeight - innerMargin;
            vBackground.height += (config.showHorizontal ? hBoxHeight - config.barThicknessInPixels : 0);

        } else if (config.location.equals(locations[UPPER_LEFT])) {
            hBackground.x = outerMargin + (config.showVertical ? vBackground.width - 2*innerMargin - config.barThicknessInPixels : 0);
            hBackground.y = outerMargin;
            vBackground.x = outerMargin;
            vBackground.y = outerMargin + (hTextTop ? hBoxHeight - config.barThicknessInPixels : 0);
            hBackground.width += (config.showVertical ? vBoxWidth - config.barThicknessInPixels : 0);
            hBackground.x -= (config.showVertical ? vBoxWidth - config.barThicknessInPixels : 0);

        } else if (config.location.equals(locations[LOWER_LEFT])) {
            hBackground.x = outerMargin + (config.showVertical ? vBackground.width - 2*innerMargin - config.barThicknessInPixels : 0);
            hBackground.y = imageHeight - outerMargin - innerMargin - hBoxHeight - innerMargin;
            vBackground.x = outerMargin;
            vBackground.y = imageHeight - outerMargin - innerMargin - hBoxHeight + (config.showHorizontal ? config.barThicknessInPixels : 0) - vBoxHeight - innerMargin;
            vBackground.height += (config.showHorizontal ? hBoxHeight - config.barThicknessInPixels : 0);

        } else {
            if (!userRoiExists)
                throw new MissingRoiException();

            hBackground.x = roiX;
            hBackground.y = roiY;
            vBackground.x = roiX;
            vBackground.y = roiY;
        }
    }

    /**
     * Sets the rectangles x y positions for scalebar elements (hBar, hText, vBar, vText),
     * based on the current configuration. Also sets the width and height of the rectangles.
     * 
     * The position of each rectangle is relative to hBackground and vBackground,
     * so setBackgroundBoxesPositions() must run before this method computes positions.
     * This method calls setBackgroundBoxesPositions().
     */
    void setElementsPositions(ImageProcessor ip) throws MissingRoiException {

        setBackgroundBoxesPositions(ip);

        int hBoxWidth = getHBoxWidthInPixels();
        int hBoxHeight = getHBoxHeightInPixels();

        int vBoxWidth = getVBoxWidthInPixels();
        int vBoxHeight = getVBoxHeightInPixels();

        int innerMargin = getInnerMarginSizeInPixels();

        boolean right = config.location.equals(locations[LOWER_RIGHT]) || config.location.equals(locations[UPPER_RIGHT]);
        boolean upper = config.location.equals(locations[UPPER_RIGHT]) || config.location.equals(locations[UPPER_LEFT]);
        boolean hTextTop = config.showVertical && upper;
        
        hBar.x = hBackground.x + innerMargin + (hBoxWidth - hBarWidthInPixels)/2 + (config.showVertical && !right && upper ? vBoxWidth - config.barThicknessInPixels : 0);
        hBar.y = hBackground.y + innerMargin + (hTextTop ? hBoxHeight - config.barThicknessInPixels : 0);
        hBar.width = hBarWidthInPixels;
        hBar.height = config.barThicknessInPixels;

        hText.height = config.hideText ? 0 : config.fontSize;
        hText.width = config.hideText ? 0 : ip.getStringWidth(getHLabel());
        hText.x = hBackground.x + innerMargin + (hBoxWidth - hText.width)/2 + (config.showVertical && !right && upper ? vBoxWidth - config.barThicknessInPixels : 0);
        hText.y = hTextTop ? (hBackground.y + innerMargin - (int)Math.round(config.fontSize*0.25)) : (hBar.y + hBar.height);

        vBar.width = config.barThicknessInPixels;
        vBar.height = vBarHeightInPixels;
        vBar.x = vBackground.x + (right ? innerMargin : vBackground.width - config.barThicknessInPixels - innerMargin);
        vBar.y = vBackground.y + innerMargin + (vBoxHeight - vBar.height)/2;

        vText.height = config.hideText ? 0 : ip.getStringWidth(getVLabel());
        vText.width = config.hideText ? 0 : config.fontSize;
        vText.x = right ? (vBar.x + vBar.width) : (vBar.x - vBoxWidth + config.barThicknessInPixels - (int)Math.round(config.fontSize*0.25));
        vText.y = vBackground.y + innerMargin + (vBoxHeight - vText.height)/2;
    }

    Color getColor() {
        Color c = Color.black;
        if (config.color.equals(colors[0])) c = Color.white;
        else if (config.color.equals(colors[2])) c = Color.lightGray;
        else if (config.color.equals(colors[3])) c = Color.gray;
        else if (config.color.equals(colors[4])) c = Color.darkGray;
        else if (config.color.equals(colors[5])) c = Color.red;
        else if (config.color.equals(colors[6])) c = Color.green;
        else if (config.color.equals(colors[7])) c = Color.blue;
        else if (config.color.equals(colors[8])) c = Color.yellow;
       return c;
    }

    // Div., mimic getColor to write getBColor for bkgnd    
    Color getBColor() {
        if (config.bcolor==null || config.bcolor.equals(bcolors[0])) return null;
        Color bc = Color.white;
        if (config.bcolor.equals(bcolors[1])) bc = Color.black;
        else if (config.bcolor.equals(bcolors[3])) bc = Color.darkGray;
        else if (config.bcolor.equals(bcolors[4])) bc = Color.gray;
        else if (config.bcolor.equals(bcolors[5])) bc = Color.lightGray;
        else if (config.bcolor.equals(bcolors[6])) bc = Color.yellow;
        else if (config.bcolor.equals(bcolors[7])) bc = Color.blue;
        else if (config.bcolor.equals(bcolors[8])) bc = Color.green;
        else if (config.bcolor.equals(bcolors[9])) bc = Color.red;
        return bc;
    }

    /**
     * Draw the scale bar, based on the current configuration.
     * 
     * If {previewOnly} is true, only the active slice will be
     * labeled with a scalebar. If it is false, all slices of
     * the stack will be labeled.
     * 
     * This method chooses whether to use an overlay or the
     * drawing tool to create the scalebar.
     */
    void updateScalebar(boolean previewOnly) {
        removeScalebar();

        Overlay scaleBarOverlay;
        try {
            scaleBarOverlay = createScaleBarOverlay();
        } catch (MissingRoiException e) {
            return; // Simply don't draw the scalebar.
        }

        Overlay impOverlay = imp.getOverlay();
        if (impOverlay==null) {
            impOverlay = new Overlay();
        }

        if (config.useOverlay) {
            for (Roi roi : scaleBarOverlay)
                impOverlay.add(roi);
            imp.setOverlay(impOverlay);
        } else {
            if (previewOnly) {
                ImageProcessor ip = imp.getProcessor();
                drawOverlayOnProcessor(scaleBarOverlay, ip);
                imp.updateAndDraw();
            } else {
                ImageStack stack = imp.getStack();
                for (int i=1; i<=stack.size(); i++) {
                    ImageProcessor ip = stack.getProcessor(i);
                    drawOverlayOnProcessor(scaleBarOverlay, ip);
                    imp.updateAndDraw();
                }
                imp.setStack(stack);
            }
        }
    }

    void drawOverlayOnProcessor(Overlay overlay, ImageProcessor processor) {
        if (processor.getBitDepth() == 8 || processor.getBitDepth() == 24) {
            // drawOverlay() only works for 8-bits and RGB
            processor.drawOverlay(overlay);
            return;
        }
        // Generate a buffer ImageProcessor, completely black.
        // We will draw the overlay on it, then loop over each pixel,
        // to replace drawn pixels in the original processor.
        ImageProcessor ip = new ByteProcessor(imp.getWidth(), imp.getHeight());
        ip.setCalibrationTable(processor.getCalibrationTable());
        LUT lut = ip.getLut();
        for (Roi roi : overlay) {
            Color fillColor = roi.getFillColor();
            if (fillColor != null) {
                int i = processor.getBestIndex(fillColor);
                roi.setFillColor(new Color(lut.getRed(i), lut.getGreen(i), lut.getBlue(i)));
                // Below, when looping on each pixel of the buffer, we detect whether it was
                // drawn by checking if the pixel value is greater than zero. Hence, we cannot
                // put zero-valued pixels when the Roi is black, or these pixels will not
                // be part of the drawing.
                if (roi.getFillColor().equals(Color.BLACK)) roi.setFillColor(new Color(1, 1, 1));
            }
            Color strokeColor = roi.getStrokeColor();
            if (strokeColor != null) {
                int i = processor.getBestIndex(strokeColor);
                roi.setStrokeColor(new Color(lut.getRed(i), lut.getGreen(i), lut.getBlue(i)));
                if (roi.getStrokeColor().equals(Color.BLACK)) roi.setStrokeColor(new Color(1, 1, 1));
            }
        }
        ip.drawOverlay(overlay);
        for (int y = 0; y < ip.getHeight(); y++)
            for (int x = 0; x < ip.getWidth(); x++) {
                int p = ip.get(x, y);
                if (p > 0) {
                    p = (int)Math.round(p * ((processor.getMax() - processor.getMin()) / 255d) + (float)processor.getMin());
                    if (processor.getBitDepth() == 32)
                        p = Float.floatToIntBits(p);
                    processor.putPixel(x, y, p);
                }
            }
    }

   class BarDialog extends GenericDialog {

        BarDialog(String hUnits, String vUnits, int hDigits, int vDigits, boolean multipleSlices) {
            super("Scale Bar");

            addNumericField("Width in "+hUnits+": ", config.hBarWidth, hDigits);
            addNumericField("Height in "+vUnits+": ", config.vBarHeight, vDigits);
            addNumericField("Thickness in pixels: ", config.barThicknessInPixels, 0);
            addNumericField("Font size: ", config.fontSize, 0);
            addChoice("Color: ", colors, config.color);
            addChoice("Background: ", bcolors, config.bcolor);
            addChoice("Location: ", locations, config.location);
            checkboxStates[0] = config.showHorizontal; checkboxStates[1] = config.showVertical;
            checkboxStates[2] = config.boldText; checkboxStates[3] = config.hideText;
            checkboxStates[4] = config.serifFont; checkboxStates[5] = config.useOverlay;
            setInsets(10, 25, 0);
            addCheckboxGroup(3, 2, checkboxLabels, checkboxStates);

            // For simplicity of the itemStateChanged() method below,
            // is is best to keep the "Label all slices" checkbox in
            // the last position.
            if (multipleSlices) {
                setInsets(0, 25, 0);
                addCheckbox("Label all slices", config.labelAll);
            }
        }
   } //BarDialog inner class

    class BarDialogListener implements DialogListener {

        boolean multipleSlices;

        public BarDialogListener(boolean multipleSlices) {
            super();
            this.multipleSlices = multipleSlices;
        }

        @Override
        public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) {
            config.hBarWidth = gd.getNextNumber();
            config.vBarHeight = gd.getNextNumber();
            config.barThicknessInPixels = (int)gd.getNextNumber();
            config.fontSize = (int)gd.getNextNumber();
            config.color = gd.getNextChoice();
            config.bcolor = gd.getNextChoice();
            config.location = gd.getNextChoice();
            config.showHorizontal = gd.getNextBoolean();
            config.showVertical = gd.getNextBoolean();
            config.boldText = gd.getNextBoolean();
            config.hideText = gd.getNextBoolean();
            config.serifFont = gd.getNextBoolean();
            config.useOverlay = gd.getNextBoolean();
            if (multipleSlices)
                config.labelAll = gd.getNextBoolean();
            if (!config.showHorizontal && !config.showVertical && IJ.isMacro()) {
                // Previous versions of this plugin did not handle vertical scale bars:
                // the macro syntax was different in that "height" meant "thickness" of
                // the horizontal scalebar.
                // If the conditional above is true, then the macro syntax is the old
                // one, so we swap a few config variables.
                config.showHorizontal = true;
                config.barThicknessInPixels = (int)config.vBarHeight;
                config.vBarHeight = 0.0;
            }

            String widthString = ((TextField) gd.getNumericFields().elementAt(0)).getText();
            boolean hasDecimalPoint = false;
            config.hDigits = 0;
            for (int i = 0; i < widthString.length(); i++) {
                if (hasDecimalPoint)
                    config.hDigits += 1;
                if (widthString.charAt(i) == '.')
                    hasDecimalPoint = true;
            }

            String heightString = ((TextField) gd.getNumericFields().elementAt(1)).getText();
            hasDecimalPoint = false;
            config.vDigits = 0;
            for (int i = 0; i < heightString.length(); i++) {
                if (hasDecimalPoint)
                    config.vDigits += 1;
                if (heightString.charAt(i) == '.')
                    hasDecimalPoint = true;
            }

            updateScalebar(true);
            return true;
        }
    }

   class MissingRoiException extends Exception {
        MissingRoiException() {
            super("Scalebar location is set to AT_SELECTION but there is no selection on the image.");
        }
   } //MissingRoiException inner class

    static class ScaleBarConfiguration {
    
        private static int defaultBarHeight = 4;

        boolean showHorizontal;
        boolean showVertical;
        double hBarWidth;
        double vBarHeight;
        int hDigits;  // The number of digits after the decimal point that the user input in the dialog for vBarWidth.
        int vDigits;
        int barThicknessInPixels;
        String location;
        String color;
        String bcolor;
        boolean boldText;
        boolean hideText;
        boolean serifFont;
        boolean useOverlay;
        int fontSize;
        boolean labelAll;

        /**
         * Create ScaleBarConfiguration with default values.
         */
        ScaleBarConfiguration() {
            this.showHorizontal = true;
            this.showVertical = false;
            this.hBarWidth = -1;
            this.vBarHeight = -1;
            this.barThicknessInPixels = defaultBarHeight;
            this.location = locations[LOWER_RIGHT];
            this.color = colors[0];
            this.bcolor = bcolors[0];
            this.boldText = true;
            this.hideText = false;
            this.serifFont = false;
            this.useOverlay = true;
            this.fontSize = 14;
            this.labelAll = false;
        }

        /**
         * Copy constructor.
         */
        ScaleBarConfiguration(ScaleBarConfiguration model) {
            this.updateFrom(model);
        }
        
        void updateFrom(ScaleBarConfiguration model) {
            this.showHorizontal = model.showHorizontal;
            this.showVertical = model.showVertical;
            this.hBarWidth = model.hBarWidth;
            this.vBarHeight = model.vBarHeight;
            this.hDigits = model.hDigits;
            this.vDigits = model.vDigits;
            this.barThicknessInPixels = model.barThicknessInPixels;
            this.location = locations[LOWER_RIGHT];
            this.color = model.color;
            this.bcolor = model.bcolor;
            this.boldText = model.boldText;
            this.serifFont = model.serifFont;
            this.hideText = model.hideText;
            this.useOverlay = model.useOverlay;
            this.fontSize = model.fontSize;
            this.labelAll = model.labelAll;
        }
    } //ScaleBarConfiguration inner class

} //ScaleBar class