edu.ku.brc.specify.tasks.subpane.wb.ImageFrame.java Source code

Java tutorial

Introduction

Here is the source code for edu.ku.brc.specify.tasks.subpane.wb.ImageFrame.java

Source

/* Copyright (C) 2015, University of Kansas Center for Research
 * 
 * Specify Software Project, specify@ku.edu, Biodiversity Institute,
 * 1345 Jayhawk Boulevard, Lawrence, Kansas, 66045, USA
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
package edu.ku.brc.specify.tasks.subpane.wb;

import static edu.ku.brc.ui.UIHelper.createButton;
import static edu.ku.brc.ui.UIHelper.createLabel;
import static edu.ku.brc.ui.UIHelper.createProgressBar;
import static edu.ku.brc.ui.UIRegistry.getResourceString;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.DefaultListCellRenderer;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import org.apache.commons.io.FilenameUtils;
import org.apache.log4j.Logger;

import com.jgoodies.forms.builder.PanelBuilder;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;

import edu.ku.brc.af.core.Taskable;
import edu.ku.brc.af.core.UsageTracker;
import edu.ku.brc.af.prefs.AppPreferences;
import edu.ku.brc.helpers.ImageFilter;
import edu.ku.brc.helpers.SwingWorker;
import edu.ku.brc.specify.datamodel.Agent;
import edu.ku.brc.specify.datamodel.Workbench;
import edu.ku.brc.specify.datamodel.WorkbenchRow;
import edu.ku.brc.specify.datamodel.WorkbenchRowImage;
import edu.ku.brc.specify.tasks.WorkbenchTask;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadTable;
import edu.ku.brc.specify.ui.HelpMgr;
import edu.ku.brc.ui.ChooseFromListDlg;
import edu.ku.brc.ui.DefaultModifiableListModel;
import edu.ku.brc.ui.GraphicsUtils;
import edu.ku.brc.ui.IconManager;
import edu.ku.brc.ui.JStatusBar;
import edu.ku.brc.ui.UIHelper;
import edu.ku.brc.ui.UIRegistry;
import edu.ku.brc.ui.IconManager.IconSize;
import edu.ku.brc.util.Pair;
import edu.ku.brc.util.thumbnails.ImageThumbnailGenerator;

/**
 * This frame is used to display a set of images linked to a record in a workbench data set.
 * 
 * @author jstewart
 *
 * @code_status Beta
 */
@SuppressWarnings("serial")
public class ImageFrame extends JFrame implements PropertyChangeListener {
    private static final Logger log = Logger.getLogger(ImageFrame.class);

    protected JProgressBar progress = createProgressBar();
    protected WorkbenchRow row;
    protected WorkbenchPaneSS wbPane;
    protected int imageIndex = -1;
    protected Workbench workbench;
    protected WorkbenchTask workbenchTask;

    // the three things that are viewed in the main display area of the frame
    protected JLabel cardImageLabel = createLabel("", SwingConstants.CENTER);
    protected JPanel noCardImageMessagePanel;
    protected JPanel noRowSelectedMessagePanel;

    protected JButton loadImgBtn;
    protected JPanel mainPane;
    protected JScrollPane scrollPane;
    protected JStatusBar statusBar;
    protected ThumbnailTray tray;

    protected JMenu viewMenu;
    protected JMenu imageMenu;
    protected JMenuItem closeMI;
    protected JMenuItem replaceMI;
    protected JMenuItem deleteMI;
    protected JMenuItem addMI;
    protected JCheckBoxMenuItem alwaysOnTopMI;
    protected JRadioButtonMenuItem origMI;
    protected JRadioButtonMenuItem reduceMI;

    protected ImageIcon defaultThumbIcon;

    protected ImageThumbnailGenerator thumbnailer;

    protected boolean allowCloseWindow;

    protected static int REDUCED_SIZE = -1;
    protected static int FULL_SIZE = 1;

    /** This hash keeps track of the size that a given image was last displayed as.  If an image is displayed as full size, we add an entry
     * to this hash where the key is row.hashCode() and the value is 1.  If the image is displayed as reduced size, the value is -1.  (We
     * don't hash from WorkbenchRows to the size in order to not hold a handle to the WorkbenchRow objects.)
     */
    protected Hashtable<Integer, Integer> rowToImageSizeHash = new Hashtable<Integer, Integer>();

    /**
     * Constructor. 
     */
    public ImageFrame(final int mapSize, final WorkbenchPaneSS wbPane, final Workbench workbench,
            final Taskable task, final boolean isReadOnly) {
        this.wbPane = wbPane;
        this.workbench = workbench;

        try {
            this.workbenchTask = (WorkbenchTask) task;
        } catch (ClassCastException e) {
            this.workbenchTask = null;
        }

        this.allowCloseWindow = true;
        this.defaultThumbIcon = IconManager.getIcon("image", IconSize.Std32);

        setIconImage(IconManager.getImage("AppIcon").getImage());

        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                if (allowCloseWindow) {
                    wbPane.toggleImageFrameVisible();
                }
            }
        });

        Dimension minSize = new Dimension(mapSize, mapSize);
        cardImageLabel.setHorizontalTextPosition(SwingConstants.CENTER);

        PanelBuilder builder = new PanelBuilder(new FormLayout("f:p:g,c:p,f:p:g", "f:p:g,p,5px,p,f:p:g"));
        CellConstraints cc = new CellConstraints();

        loadImgBtn = createButton(getResourceString("WB_LOAD_NEW_IMAGE"));

        builder.add(createLabel(getResourceString("WB_NO_IMAGE_ROW"), SwingConstants.CENTER), cc.xy(2, 2));
        builder.add(loadImgBtn, cc.xy(2, 4));
        loadImgBtn.setVisible(!isReadOnly);

        noCardImageMessagePanel = builder.getPanel();
        noCardImageMessagePanel.setPreferredSize(minSize);
        noCardImageMessagePanel.setSize(minSize);

        builder = new PanelBuilder(new FormLayout("f:p:g,c:p,f:p:g", "f:p:g,c:p,f:p:g"));
        builder.add(createLabel(getResourceString("WB_NO_ROW_SELECTED"), SwingConstants.CENTER), cc.xy(2, 2));

        noRowSelectedMessagePanel = builder.getPanel();
        noRowSelectedMessagePanel.setPreferredSize(minSize);
        noRowSelectedMessagePanel.setSize(minSize);

        mainPane = new JPanel(new BorderLayout());
        mainPane.setSize(minSize);
        mainPane.setPreferredSize(minSize);
        mainPane.setMinimumSize(minSize);

        mainPane.add(cardImageLabel, BorderLayout.CENTER);
        scrollPane = new JScrollPane(mainPane, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);

        statusBar = new JStatusBar();

        thumbnailer = new ImageThumbnailGenerator();
        thumbnailer.setMaxSize(256, 256);
        thumbnailer.setQuality(1);

        tray = new ThumbnailTray();
        tray.getModel().removeAllElements();
        tray.addListSelectionListener(new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent e) {
                if (e.getValueIsAdjusting()) {
                    return;
                }

                setImageIndex(tray.getSelectedIndex());
            }
        });

        JPanel southPanel = new JPanel();
        southPanel.setLayout(new BorderLayout());

        //southPanel.add(indexSlider,BorderLayout.NORTH);
        southPanel.add(tray, BorderLayout.CENTER);
        southPanel.add(statusBar, BorderLayout.SOUTH);

        JPanel basePanel = new JPanel();
        basePanel.setLayout(new BorderLayout());
        basePanel.add(scrollPane, BorderLayout.CENTER);
        basePanel.add(southPanel, BorderLayout.SOUTH);

        setContentPane(basePanel);

        JMenuBar menuBar = new JMenuBar();

        String title = "FileMenu";
        String mneu = "FileMneu";
        JMenu fileMenu = UIHelper.createLocalizedMenu(menuBar, title, mneu);
        title = "WB_IMPORT_CARDS_TO_DATASET";
        mneu = "WB_IMPORT_CARDS_MNEU";

        if (!isReadOnly) {
            JMenuItem importImagesMI = UIHelper.createLocalizedMenuItem(fileMenu, title, mneu, "", true, null);
            importImagesMI.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent ae) {

                    importImages();
                }
            });
        }

        /*        title = "ImageFrame.CLOSE";
                mneu = "ImageFrame.CloseMneu";
                closeMI = UIHelper.createLocalizedMenuItem(fileMenu, title, mneu,  "", true, null);
                closeMI.addActionListener(new ActionListener()
                {
        public void actionPerformed(ActionEvent ae)
        {
            ImageFrame.this.setVisible(false);
        }
                });*/

        title = "ImageFrame.View";
        mneu = "ImageFrame.ViewMneu";
        viewMenu = UIHelper.createLocalizedMenu(menuBar, title, mneu);
        reduceMI = UIHelper.createRadioButtonMenuItem(viewMenu, "WB_REDUCED_SIZE", "ImageFrame.ReducedSizeMneu", "",
                true, null);
        reduceMI.setSelected(true);
        reduceMI.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (row == null) {
                    return;
                }

                // simply 'remember' that we want to show reduced images for this row
                rowToImageSizeHash.put(row.hashCode(), REDUCED_SIZE);
                // then 'reshow' the current image
                showImage();
            }
        });

        origMI = UIHelper.createRadioButtonMenuItem(viewMenu, "WB_ORIG_SIZE", "ImageFrame.OrigMneu", "", true,
                null);
        origMI.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (row == null) {
                    return;
                }

                // simply 'remember' that we want to show fill size images for this row
                rowToImageSizeHash.put(row.hashCode(), FULL_SIZE);
                // then 'reshow' the current image
                showImage();
            }
        });

        ButtonGroup btnGrp = new ButtonGroup();
        btnGrp.add(reduceMI);
        btnGrp.add(origMI);

        viewMenu.addSeparator();

        alwaysOnTopMI = UIHelper.createCheckBoxMenuItem(viewMenu, "WB_ALWAYS_ON_TOP", null, "", true, null);
        alwaysOnTopMI.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
                ImageFrame.this.setAlwaysOnTop(alwaysOnTopMI.isSelected());
            }
        });

        addPropertyChangeListener("alwaysOnTop", this);

        if (!isReadOnly) {
            ActionListener deleteImg = new ActionListener() {
                public void actionPerformed(ActionEvent ae) {
                    deleteImage();
                }
            };

            ActionListener replaceImg = new ActionListener() {
                public void actionPerformed(ActionEvent ae) {
                    replaceImage();
                }
            };

            ActionListener addImg = new ActionListener() {
                public void actionPerformed(ActionEvent ae) {
                    addImages();
                }
            };

            title = "ImageFrame.Image";
            mneu = "ImageFrame.ImageMneu";
            imageMenu = UIHelper.createLocalizedMenu(menuBar, title, mneu);

            title = "WB_ADD_IMG";
            mneu = "WB_ADD_IMG_MNEM";
            addMI = UIHelper.createLocalizedMenuItem(imageMenu, title, mneu, "", true, addImg);

            title = "WB_REPLACE_IMG";
            mneu = "WB_REPLACE_IMG_MNEU";
            replaceMI = UIHelper.createLocalizedMenuItem(imageMenu, title, mneu, "", true, replaceImg);

            title = "WB_DEL_IMG_LINK";
            mneu = "WB_DEL_IMG_LINK_MNEU";
            deleteMI = UIHelper.createLocalizedMenuItem(imageMenu, title, mneu, "", true, deleteImg);

            loadImgBtn.addActionListener(addImg);
        }

        JMenu helpMenu = new JMenu(getResourceString("HELP"));
        menuBar.add(HelpMgr.createHelpMenuItem(helpMenu, getResourceString("WB_IMAGE_WINDOW")));

        enableMenus(false);

        setJMenuBar(menuBar);

        pack();

        HelpMgr.setHelpID(this, "WorkbenchWorkingWithImages");
    }

    public void propertyChange(PropertyChangeEvent evt) {
        if (evt.getPropertyName().equals("alwaysOnTop") && evt.getSource() == this) {
            // the alwaysOnTop bahavior of the window changed... update the menu item
            alwaysOnTopMI.setSelected(this.isAlwaysOnTop());
        }
    }

    /**
     * @param workbench the workbench to set
     */
    public void setWorkbench(Workbench workbench) {
        this.workbench = workbench;
    }

    /**
     * NOTE (IMPORTANT): This line of code needs to be in it's own method because it was causing a memory leak
     * by referencing one of the two data members. Couldn't figure out why the Garbage Collector
     * wants to hold onto the entire ImageFrame because of it.
     */
    protected void importImages() {
        if (workbenchTask != null) {
            workbenchTask.importCardImages(workbench, true);
        }
    }

    /**
     * Sets text into the statubar of the image frame.
     * @param text the message
     */
    public void setStatusBarText(final String text) {
        statusBar.setText(text);
    }

    /**
     * Returns the index of the selected image
     * 
     * @return
     */
    public int getImageIndex() {
        return imageIndex;
    }

    /**
     * @param imageIndex
     */
    public void setImageIndex(int imageIndex) {
        this.imageIndex = imageIndex;
        tray.setSelectedIndex(imageIndex);
        showImage();
    }

    protected void showImage() {
        if (row == null) {
            return;
        }

        Set<WorkbenchRowImage> rowImages = row.getWorkbenchRowImages();
        if (rowImages == null || rowImages.size() == 0) {
            noImagesLinked();
            return;
        }

        // adjust the imageIndex to be within the proper bounds (in order to avoid a few error possibilities)
        if (imageIndex < 0) {
            imageIndex = 0;
            tray.setSelectedIndex(imageIndex);
        } else if (imageIndex > rowImages.size() - 1) {
            imageIndex = rowImages.size() - 1;
            tray.setSelectedIndex(imageIndex);
        }

        // try to get the appropriate WorkbenchRowImage
        WorkbenchRowImage rowImage = row.getRowImage(imageIndex);
        if (rowImage == null) {
            // What do we do here?
            // This should never happen.
            // There were images available, but none of them had the right index.
            // Just give the first one, I guess.
            rowImage = rowImages.iterator().next();
            imageIndex = 0;
            tray.setSelectedIndex(imageIndex);
            statusBar.setWarningMessage("Unable to locate an image with the proper index.  Showing first image."); // XXX i18n
        }

        // at this point, we know at least one image is available

        // update the title and status bar
        String fullFilePath = rowImage.getCardImageFullPath();
        String filename = FilenameUtils.getName(fullFilePath);
        setTitle(String.format(getResourceString("WB_IMAGE_X_OF_Y"), imageIndex + 1, rowImages.size())
                + ((filename != null) ? ": " + filename : ""));
        setStatusBarText(fullFilePath);

        ImageIcon image = null;

        Integer lastDisplayedSize = rowToImageSizeHash.get(row.hashCode());
        if (lastDisplayedSize == null || lastDisplayedSize == REDUCED_SIZE) {
            image = rowImage.getReducedImage();
            reduceMI.setSelected(true);
        } else {
            image = rowImage.getFullSizeImage();
            origMI.setSelected(true);
        }

        cardImageLabel.setIcon(image);

        if (image == null) // no image available
        {
            statusBar.setErrorMessage("Unable to load image"); // XXX i18n
        } else // we've got an image
        {
            // this method is simpler than tracking what we're showing and changing to the correct view
            mainPane.remove(noRowSelectedMessagePanel);
            mainPane.remove(noCardImageMessagePanel);
            mainPane.remove(cardImageLabel);
            mainPane.add(cardImageLabel);

            int w = image.getIconWidth();
            int h = image.getIconHeight();
            cardImageLabel.setText(null);
            cardImageLabel.setSize(w, h);
            mainPane.setSize(w, h);
            mainPane.setPreferredSize(new Dimension(w, h));
            enableMenus(true);

            mainPane.validate();
            mainPane.repaint();
        }
    }

    /**
     * Handles UI tasks related to showing the user that no images are available for display.
     */
    protected void noImagesLinked() {
        // this method is simpler than tracking what we're showing and changing to the correct view
        mainPane.remove(noRowSelectedMessagePanel);
        mainPane.remove(noCardImageMessagePanel);
        mainPane.remove(cardImageLabel);
        mainPane.add(noCardImageMessagePanel);

        tray.getModel().removeAllElements();
        setTitle(getResourceString("WB_NO_IMAGES_LINKED"));
        setStatusBarText(null);
        enableMenus(false);
        Dimension prefSize = noCardImageMessagePanel.getPreferredSize();
        mainPane.setSize(prefSize);
        mainPane.setPreferredSize(prefSize);
        validate();
        repaint();
    }

    protected void noRowSelected() {
        // this method is simpler than tracking what we're showing and changing to the correct view
        mainPane.remove(noRowSelectedMessagePanel);
        mainPane.remove(noCardImageMessagePanel);
        mainPane.remove(cardImageLabel);
        mainPane.add(noRowSelectedMessagePanel);

        tray.getModel().removeAllElements();
        setTitle(getResourceString("WB_NO_ROW_SELECTED"));
        setStatusBarText(null);
        enableMenus(false);
        Dimension prefSize = noRowSelectedMessagePanel.getPreferredSize();
        mainPane.setSize(prefSize);
        mainPane.setPreferredSize(prefSize);
        validate();
        repaint();
    }

    /**
     * Enables Menus per image state.
     * @param enable true/false
     */
    protected void enableMenus(final boolean enable) {
        if (addMI != null) {
            addMI.setEnabled(enable);
        }
        if (deleteMI != null) {
            deleteMI.setEnabled(enable);
        }
        if (replaceMI != null) {
            replaceMI.setEnabled(enable);
        }
        origMI.setEnabled(enable);
        reduceMI.setEnabled(enable);
    }

    /**
     * Makes sure the image can be read and it is not corrupted.
     * @param srcFile the full path to the image
     * @return true if ok
     */
    public static boolean testImageFile(final String srcFile) {
        try {
            byte[] bytes = GraphicsUtils.readImage(srcFile);
            if (bytes != null) {
                ImageIcon testIcon = new ImageIcon(bytes);

                //System.err.println(testIcon.getIconHeight()+"  "+testIcon.getIconWidth());

                // this image file is corrupted or a format that we cannot display
                return testIcon.getIconHeight() > 0 && testIcon.getIconWidth() > 0;
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    /**
     * @return 
     */
    protected String getAttachToTableName() {
        String attachToTableName = null;
        List<UploadTable> attachableTbls = wbPane.getAttachableTables();
        if (attachableTbls == null || attachableTbls.size() == 0) {
            //XXX message about default attachment table
        } else {
            if (attachableTbls.size() == 1) {
                attachToTableName = attachableTbls.get(0).getTable().getName();
            } else {
                Vector<Pair<String, String>> titleNamePairs = new Vector<Pair<String, String>>();
                Vector<String> titles = new Vector<String>();
                for (UploadTable ut : attachableTbls) {
                    String tblTitle = ut.getTblClass().equals(Agent.class) ? ut.toString() : ut.getTblTitle();
                    if (!titles.contains(tblTitle))
                        ;
                    {
                        String tblSpec = ut.getTable().getName();
                        if (ut.getTblClass().equals(Agent.class) && ut.getRelationship() != null) {
                            tblSpec += "."
                                    + ut.getRelationship().getRelatedField().getTable().getTableInfo().getName();
                        }
                        titleNamePairs.add(new Pair<String, String>(tblTitle, tblSpec));
                        titles.add(tblTitle);
                    }
                }
                ChooseFromListDlg<String> dlg = new ChooseFromListDlg<String>(
                        (Frame) UIRegistry.getMostRecentWindow(),
                        UIRegistry.getResourceString("ImageFrame.ChooseAttachTableDlgTitle"), titles);
                UIHelper.centerAndShow(dlg);
                if (!dlg.isCancelled()) {
                    for (Pair<String, String> p : titleNamePairs) {
                        if (p.getFirst().equals(dlg.getSelectedObject())) {
                            attachToTableName = p.getSecond();
                            break;
                        }
                    }

                } else {
                    attachToTableName = "";
                }

                dlg.dispose();
            }
        }
        return attachToTableName;
    }

    /**
     * Adds a new image to the current {@link WorkbenchRow}.
     */
    public void addImages() {
        UsageTracker.incrUsageCount("WB.AddWBRowImage");

        final WorkbenchRow wbRow = this.row;
        final String attachToTableName = getAttachToTableName();
        if ("".equals(attachToTableName)) {
            return; //choose attachToTable dlg cancelled by user.
        }

        final File[] imageFiles = askUserForImageFiles();
        if (imageFiles == null || imageFiles.length == 0) {
            return;
        }
        for (File f : imageFiles) {
            if (!testImageFile(f.getAbsolutePath())) {
                JOptionPane.showMessageDialog(UIRegistry.getMostRecentWindow(),
                        String.format(getResourceString("WB_WRONG_IMAGE_TYPE_OR_CORRUPTED_IMAGE"),
                                new Object[] { f.getAbsolutePath() }),
                        UIRegistry.getResourceString("WARNING"), JOptionPane.ERROR_MESSAGE);
                return;
            }
        }

        allowCloseWindow = false;
        this.setEnabled(false);

        log.debug("addImages: " + imageFiles.length + " files selected");

        loadImgBtn.setEnabled(false);

        SwingWorker loadImagesTask = new SwingWorker() {
            private List<WorkbenchRowImage> rowImagesNeedingThumbnails = new Vector<WorkbenchRowImage>();

            @Override
            public Object construct() {
                Vector<Integer> newIndexes = new Vector<Integer>();
                for (int i = 0; i < imageFiles.length; ++i) {
                    final int index = i;
                    File f = imageFiles[i];
                    try {
                        int newIndex = wbRow.addImage(f);
                        if (newIndex > -1) {
                            newIndexes.add(newIndex);

                            SwingUtilities.invokeLater(new Runnable() {
                                public void run() {
                                    UIRegistry.writeGlassPaneMsg(
                                            String.format(getResourceString("WB_ADDING_IMAGE_X_OF_Y"), index + 1,
                                                    imageFiles.length),
                                            24);
                                }
                            });

                            WorkbenchRowImage rowImage = row.getRowImage(newIndex);
                            rowImage.setAttachToTableName(attachToTableName);
                            rowImagesNeedingThumbnails.add(rowImage);
                            wbPane.setChanged(true);
                        }
                    } catch (IOException e) {
                        statusBar.setErrorMessage("Exception while adding a new image", e);
                    }
                }
                Collections.sort(newIndexes);
                return newIndexes;
            }

            @SuppressWarnings("unchecked")
            @Override
            public void finished() {
                loadImgBtn.setEnabled(true);

                Object retVal = get();

                if (retVal != null && retVal instanceof List && ((List) retVal).size() > 0) {
                    Vector<Integer> newIndexes = (Vector<Integer>) retVal;
                    int lowestNewIndex = newIndexes.get(0);

                    // add a bunch of placeholder icons
                    for (int i = 0; i < newIndexes.size(); ++i) {
                        tray.getModel().add(defaultThumbIcon);
                    }
                    wbPane.setChanged(true);

                    if (lowestNewIndex < row.getWorkbenchRowImages().size()) {
                        imageIndex = lowestNewIndex;
                        tray.setSelectedIndex(imageIndex);
                        showImage();
                    }

                    generateThumbnailsInBackground(rowImagesNeedingThumbnails);
                }

                UIRegistry.clearGlassPaneMsg();
                setEnabled(true);
                allowCloseWindow = true;
                if (wbPane.getIncremental()) {
                    wbPane.updateRowValidationStatus(wbRow.getRowNumber(), -1, null);
                }
                wbPane.repaint();
            }
        };
        loadImagesTask.start();
    }

    /**
     * 
     */
    public void replaceImage() {
        UsageTracker.incrUsageCount("WB.EditWBRowImage");
        File imageFile = askUserForImageFile();
        if (imageFile == null) {
            // user cancelled
            return;
        }

        log.debug("replaceImage: " + imageFile.getAbsolutePath());
        try {
            row.setImage(imageIndex, imageFile);
            wbPane.setChanged(true);
            if (wbPane.getIncremental()) {
                wbPane.updateRowValidationStatus(row.getRowNumber(), -1, null);
            }

            WorkbenchRowImage rowImage = row.getRowImage(imageIndex);
            Vector<WorkbenchRowImage> needNewThumbs = new Vector<WorkbenchRowImage>();
            needNewThumbs.add(rowImage);
            generateThumbnailsInBackground(needNewThumbs);

            // call showImage() to update the visible image
            showImage();
        } catch (IOException e) {
            UsageTracker.incrHandledUsageCount();
            edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(ImageFrame.class, e);
            statusBar.setErrorMessage("Exception while replacing image", e);
        }
    }

    public void deleteImage() {
        UsageTracker.incrUsageCount("WB.DeleteWBRowImage");
        row.deleteImage(imageIndex);
        tray.getModel().remove(imageIndex);
        imageIndex--;
        tray.setSelectedIndex(imageIndex);

        // call showImage() to update the visible image
        showImage();

        wbPane.setChanged(true);
        if (wbPane.getIncremental()) {
            wbPane.updateRowValidationStatus(row.getRowNumber(), -1, null);
        }
        wbPane.repaint();
    }

    protected File askUserForImageFile() {
        ImageFilter imageFilter = new ImageFilter();
        JFileChooser fileChooser = new JFileChooser(
                WorkbenchTask.getDefaultDirPath(WorkbenchTask.IMAGES_FILE_PATH));
        fileChooser.setFileFilter(imageFilter);
        fileChooser.setDialogTitle(getResourceString("WB_CHOOSE_IMAGE"));
        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);

        int userAction = fileChooser.showOpenDialog(this);
        AppPreferences localPrefs = AppPreferences.getLocalPrefs();

        // remember the directory the user was last in
        localPrefs.put(WorkbenchTask.IMAGES_FILE_PATH, fileChooser.getCurrentDirectory().getAbsolutePath());

        if (userAction == JFileChooser.APPROVE_OPTION) {
            String fullPath = fileChooser.getSelectedFile().getAbsolutePath();
            if (imageFilter.isImageFile(fullPath)) {
                return fileChooser.getSelectedFile();
            }
        }

        // if for any reason (user cancelled) we got to this point...
        return null;
    }

    protected File[] askUserForImageFiles() {
        ImageFilter imageFilter = new ImageFilter();
        JFileChooser fileChooser = new JFileChooser(
                WorkbenchTask.getDefaultDirPath(WorkbenchTask.IMAGES_FILE_PATH));
        fileChooser.setFileFilter(imageFilter);
        fileChooser.setDialogTitle(getResourceString("WB_CHOOSE_IMAGES"));
        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        fileChooser.setMultiSelectionEnabled(true);

        int userAction = fileChooser.showOpenDialog(this);
        AppPreferences localPrefs = AppPreferences.getLocalPrefs();

        // remember the directory the user was last in
        localPrefs.put(WorkbenchTask.IMAGES_FILE_PATH, fileChooser.getCurrentDirectory().getAbsolutePath());

        if (userAction == JFileChooser.APPROVE_OPTION) {
            return fileChooser.getSelectedFiles();
        }

        // if for any reason we got to this point...
        return null;
    }

    /**
     * Sests the row into the frame.
     * @param row the row
     */
    public void setRow(final WorkbenchRow row) {
        // if nothing changed, ignore this call
        if (row != null && this.row == row) {
            return;
        }

        this.row = row;

        if (row == null) {
            noRowSelected();
            return;
        }

        refreshImages();
    }

    /**
     * 
     */
    public void refreshImages() {
        // put the correct thumbs in the tray UI
        tray.getModel().removeAllElements();

        if (row != null) {
            // add the default, pre-thumbnail-generation icons to the tray for each image
            Set<WorkbenchRowImage> rowImages = row.getWorkbenchRowImages();
            for (int i = 0; i < rowImages.size(); ++i) {
                tray.getModel().add(defaultThumbIcon);
            }

            // start the background thumbnail generation process
            // replacing the default icons as thumbnails become available
            List<WorkbenchRowImage> rowImagesNeedingThumbnails = new Vector<WorkbenchRowImage>();

            for (WorkbenchRowImage img : rowImages) {
                // load any cached thumbnails
                ImageIcon thumb = img.getThumbnail();
                if (thumb != null) {
                    tray.getModel().set(img.getImageOrder(), thumb);
                } else // generate any missing thumbnails
                {
                    log.debug(
                            "Workbench row image is missing its thumbnail.  Adding it to the list of row images for thumbnail generation work.  "
                                    + img);
                    rowImagesNeedingThumbnails.add(img);
                }
            }

            if (rowImagesNeedingThumbnails.size() > 0) {
                generateThumbnailsInBackground(rowImagesNeedingThumbnails);
            }

            // set the index so the first image is displayed
            imageIndex = 0;
            tray.setSelectedIndex(imageIndex);
            showImage();
        }
    }

    protected ImageIcon generateThumbnail(WorkbenchRowImage rowImage) throws IOException {
        if (thumbnailer != null) {
            File orig = new File(rowImage.getCardImageFullPath());
            byte[] origData = GraphicsUtils.readImage(orig);
            if (origData != null) {
                byte[] thumbData = thumbnailer.generateThumbnail(origData, true);
                return new ImageIcon(thumbData);
            }
        }
        return null;
    }

    protected void generateThumbnailsInBackground(final List<WorkbenchRowImage> rowImages) {
        Collections.sort(rowImages);

        Thread thumbGenTask = new Thread() {
            @Override
            @SuppressWarnings("synthetic-access")
            public void run() {
                // This is just a weird workaround.
                // For some reason, using the List directly resulted in a ConcurrentModificationException everytime
                // this method was called from addImages().
                // It doesn't look like it should throw an exception at all.
                WorkbenchRowImage[] imgs = new WorkbenchRowImage[rowImages.size()];
                rowImages.toArray(imgs);
                for (WorkbenchRowImage rowImage : imgs) {
                    final WorkbenchRowImage ri = rowImage;
                    try {
                        final ImageIcon thumb = generateThumbnail(rowImage);

                        // cache it so we don't have to do this again and again
                        rowImage.setThumbnail(thumb);

                        // update the UI
                        Runnable updateTrayUI = new Runnable() {
                            public void run() {
                                log.info("Thumbnail generation complete.  Updating the UI.  " + ri);
                                if (row == ri.getWorkbenchRow()) {
                                    tray.getModel().set(ri.getImageOrder(), thumb);
                                    tray.repaint();
                                }
                            }
                        };
                        SwingUtilities.invokeLater(updateTrayUI);
                    } catch (IOException e) {
                        UsageTracker.incrHandledUsageCount();
                        edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(ImageFrame.class, e);
                        log.warn("Failed to generate a thumbnail for " + rowImage.getCardImageFullPath(), e);
                    }
                }
            }
        };

        thumbGenTask.setName("GenThumbs");
        thumbGenTask.setDaemon(true);
        thumbGenTask.setPriority(Thread.MIN_PRIORITY);
        thumbGenTask.start();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.awt.Window#setVisible(boolean)
     */
    @Override
    public void setVisible(boolean b) {
        row = null;
        cardImageLabel.setIcon(null);
        cardImageLabel.setText(null);
        super.setVisible(b);
    }

    /**
     * Clean up references.
     */
    public void cleanUp() {
        setJMenuBar(null);
        removeAll();
        this.progress = null;
        this.row = null;
        this.wbPane = null;
        this.workbench = null;
        this.workbenchTask = null;
        this.defaultThumbIcon = null;
        this.mainPane = null;
        this.scrollPane = null;
        this.statusBar = null;
        this.tray = null;
        this.thumbnailer = null;

        setIconImage(null);
    }

    //--------------------------------------------------------------------------------------
    // Inner Classes
    //--------------------------------------------------------------------------------------

    class ThumbnailTray extends JPanel {
        /** A JList used to display the thumbnails representing the items. */
        protected JList listWidget;
        /** The model holding the included items. */
        protected DefaultModifiableListModel<Icon> listModel;
        /** A JScrollPane containing the iconListWidget. */
        protected JScrollPane listScrollPane;

        protected int minHeight = 64;

        /**
         * Creates a new IconTray containing zero items.
         */
        public ThumbnailTray() {
            listModel = new DefaultModifiableListModel<Icon>();
            ListCellRenderer renderer = new DefaultListCellRenderer() {
                @Override
                public Component getListCellRendererComponent(JList list, Object value, int index,
                        boolean isSelected, boolean cellHasFocus) {
                    JLabel l = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected,
                            cellHasFocus);
                    if (value instanceof Icon) {
                        l.setText(null);
                        l.setIcon((Icon) value);
                    }
                    return l;
                }
            };
            listWidget = new JList(listModel);
            listWidget.setCellRenderer(renderer);
            listWidget.setLayoutOrientation(JList.HORIZONTAL_WRAP);
            listWidget.setVisibleRowCount(1);
            listWidget.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

            JPanel listPanel = new JPanel();
            listPanel.setBackground(listWidget.getBackground());
            listPanel.setLayout(new BoxLayout(listPanel, BoxLayout.LINE_AXIS));
            listPanel.add(Box.createHorizontalGlue());
            listPanel.add(listWidget);
            listPanel.add(Box.createHorizontalGlue());

            listScrollPane = new JScrollPane(listPanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER,
                    ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);

            this.setLayout(new BorderLayout());
            this.add(listScrollPane, BorderLayout.CENTER);
        }

        public DefaultModifiableListModel<Icon> getModel() {
            return listModel;
        }

        /**
         * Sets the height of every cell in the list.
         *
         * @param height an integer giving the height, in pixels, for all cells in this list
         * @see JList#setFixedCellHeight(int)
         */
        public synchronized void setFixedCellHeight(int height) {
            listWidget.setFixedCellHeight(height);
        }

        /* (non-Javadoc)
         * @see javax.swing.JComponent#getPreferredSize()
         */
        @Override
        public Dimension getPreferredSize() {
            // need to set the min height to something other than 0 so
            // that empty trays don't get flattened by containers that
            // use preferred size
            int height = minHeight;
            Dimension d = super.getPreferredSize();
            if ((int) d.getHeight() > height) {
                height = (int) d.getHeight();
            }
            return new Dimension(this.getWidth(), height);
        }

        /**
         * @see JList#getSelectedIndex()
         * @return the index of the selection
         */
        public int getSelectedIndex() {
            return listWidget.getSelectedIndex();
        }

        /**
         * @see JList#setSelectedIndex(int)
         * @param index the index of the selection
         */
        public void setSelectedIndex(int index) {
            if (row == null || index < 0 || index > row.getWorkbenchRowImages().size() - 1) {
                return;
            }

            listWidget.setSelectedIndex(index);

            Rectangle cellBounds = listWidget.getUI().getCellBounds(listWidget, index, index);
            if (cellBounds != null) {
                listWidget.scrollRectToVisible(cellBounds);
            }
        }

        /**
         * @see JList#addListSelectionListener(ListSelectionListener)
         * @param listener a {@link ListSelectionListener}
         */
        public void addListSelectionListener(ListSelectionListener listener) {
            listWidget.addListSelectionListener(listener);
        }

        /**
         * @see JList#removeListSelectionListener(ListSelectionListener)
         * @param listener a {@link ListSelectionListener}
         */
        public void removeListSelectionListener(ListSelectionListener listener) {
            listWidget.removeListSelectionListener(listener);
        }
    }
}