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

Java tutorial

Introduction

Here is the source code for edu.ku.brc.specify.tasks.subpane.wb.WorkbenchPaneSS.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.createDuplicateJGoodiesDef;
import static edu.ku.brc.ui.UIHelper.createIconBtn;
import static edu.ku.brc.ui.UIHelper.createLabel;
import static edu.ku.brc.ui.UIRegistry.getResourceString;

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Queue;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultCellEditor;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SortOrder;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import javax.swing.text.Caret;
import javax.swing.text.JTextComponent;
import javax.swing.undo.UndoManager;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.dom4j.Element;
import org.jdesktop.swingx.decorator.AbstractHighlighter;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.HighlightPredicate;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.decorator.HighlighterFactory;
import org.jdesktop.swingx.table.TableColumnExt;

import sun.swing.table.DefaultTableCellHeaderRenderer;

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

import edu.ku.brc.af.core.AppContextMgr;
import edu.ku.brc.af.core.ContextMgr;
import edu.ku.brc.af.core.SubPaneIFace;
import edu.ku.brc.af.core.SubPaneMgr;
import edu.ku.brc.af.core.Taskable;
import edu.ku.brc.af.core.UsageTracker;
import edu.ku.brc.af.core.db.DBFieldInfo;
import edu.ku.brc.af.core.db.DBTableIdMgr;
import edu.ku.brc.af.core.db.DBTableInfo;
import edu.ku.brc.af.prefs.AppPreferences;
import edu.ku.brc.af.tasks.subpane.BaseSubPane;
import edu.ku.brc.af.ui.db.PickListDBAdapterIFace;
import edu.ku.brc.af.ui.db.PickListItemIFace;
import edu.ku.brc.af.ui.forms.FormHelper;
import edu.ku.brc.af.ui.forms.ResultSetController;
import edu.ku.brc.af.ui.forms.ResultSetControllerListener;
import edu.ku.brc.af.ui.forms.formatters.UIFieldFormatterIFace;
import edu.ku.brc.af.ui.forms.validation.FormValidator;
import edu.ku.brc.dbsupport.DataProviderFactory;
import edu.ku.brc.dbsupport.DataProviderSessionIFace;
import edu.ku.brc.dbsupport.RecordSetIFace;
import edu.ku.brc.dbsupport.StaleObjectException;
import edu.ku.brc.helpers.ImageFilter;
import edu.ku.brc.helpers.SwingWorker;
import edu.ku.brc.helpers.XMLHelper;
import edu.ku.brc.services.biogeomancer.GeoCoordBGMProvider;
import edu.ku.brc.services.biogeomancer.GeoCoordDataIFace;
import edu.ku.brc.services.biogeomancer.GeoCoordProviderListenerIFace;
import edu.ku.brc.services.biogeomancer.GeoCoordServiceProviderIFace;
import edu.ku.brc.services.mapping.LatLonPlacemarkIFace;
import edu.ku.brc.services.mapping.LocalityMapper;
import edu.ku.brc.services.mapping.LocalityMapper.MapLocationIFace;
import edu.ku.brc.services.mapping.LocalityMapper.MapperListener;
import edu.ku.brc.services.mapping.SimpleMapLocation;
import edu.ku.brc.specify.config.SpecifyAppContextMgr;
import edu.ku.brc.specify.datamodel.Discipline;
import edu.ku.brc.specify.datamodel.Geography;
import edu.ku.brc.specify.datamodel.Locality;
import edu.ku.brc.specify.datamodel.Preparation;
import edu.ku.brc.specify.datamodel.RecordSet;
import edu.ku.brc.specify.datamodel.Workbench;
import edu.ku.brc.specify.datamodel.WorkbenchDataItem;
import edu.ku.brc.specify.datamodel.WorkbenchRow;
import edu.ku.brc.specify.datamodel.WorkbenchTemplateMappingItem;
import edu.ku.brc.specify.dbsupport.RecordTypeCodeBuilder;
import edu.ku.brc.specify.rstools.ExportFileConfigurationFactory;
import edu.ku.brc.specify.rstools.ExportToFile;
import edu.ku.brc.specify.rstools.GoogleEarthExporter;
import edu.ku.brc.specify.rstools.WorkbenchRowPlacemarkWrapper;
import edu.ku.brc.specify.tasks.DataEntryTask;
import edu.ku.brc.specify.tasks.ExpressSearchTask;
import edu.ku.brc.specify.tasks.InteractionsTask;
import edu.ku.brc.specify.tasks.PluginsTask;
import edu.ku.brc.specify.tasks.SGRTask;
import edu.ku.brc.specify.tasks.WorkbenchTask;
import edu.ku.brc.specify.tasks.subpane.ESResultsSubPane;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.DB;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UniquenessChecker;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadData;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadField;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadMappingDef;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadMessage;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadTable;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadTableInvalidValue;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadTableMatchInfo;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadToolPanel;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.Uploader;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploaderException;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.WorkbenchUploadMapper;
import edu.ku.brc.specify.ui.HelpMgr;
import edu.ku.brc.specify.ui.LengthInputVerifier;
import edu.ku.brc.specify.ui.db.PickListDBAdapterFactory;
import edu.ku.brc.ui.CommandAction;
import edu.ku.brc.ui.CommandDispatcher;
import edu.ku.brc.ui.CustomDialog;
import edu.ku.brc.ui.DropDownButtonStateful;
import edu.ku.brc.ui.DropDownMenuInfo;
import edu.ku.brc.ui.IconManager;
import edu.ku.brc.ui.JStatusBar;
import edu.ku.brc.ui.ToggleButtonChooserDlg;
import edu.ku.brc.ui.ToggleButtonChooserPanel;
import edu.ku.brc.ui.ToggleButtonChooserPanel.Type;
import edu.ku.brc.ui.UIHelper;
import edu.ku.brc.ui.UIRegistry;
import edu.ku.brc.ui.UnhandledExceptionDialog;
import edu.ku.brc.ui.WorkBenchPluginIFace;
import edu.ku.brc.ui.dnd.SimpleGlassPane;
import edu.ku.brc.ui.tmanfe.SearchReplacePanel;
import edu.ku.brc.ui.tmanfe.SpreadSheet;
import edu.ku.brc.util.GeoRefConverter;
import edu.ku.brc.util.GeoRefConverter.GeoRefFormat;
import edu.ku.brc.util.LatLonConverter;
import edu.ku.brc.util.Pair;

/**
 * Main class that handles the editing of Workbench data. It creates both a spreasheet and a form pane for editing the data.
 * 
 * @author rods, jstewart
 *
 * @code_status Beta
 *
 * Created Date: Mar 6, 2007
 *
 */
@SuppressWarnings("serial")
public class WorkbenchPaneSS extends BaseSubPane {
    private static boolean debugging = true;
    protected static final Logger log = Logger.getLogger(WorkbenchPaneSS.class);

    final public static String wbAutoValidatePrefName = "WB.AutoValidatePref";
    final public static String wbAutoMatchPrefName = "WB.AutoMatchPref";

    public enum PanelType {
        Spreadsheet, Form
    }

    protected SearchReplacePanel findPanel = null;
    protected SpreadSheet spreadSheet;
    protected Workbench workbench;
    protected GridTableModel model;
    protected TableColumnExt imageColExt;
    protected String[] columns;
    protected Integer[] columnMaxWidths; //the maximum characters allowable for the mapped fields 
    protected Vector<WorkbenchTemplateMappingItem> headers = new Vector<WorkbenchTemplateMappingItem>();
    protected boolean hasChanged = false;
    protected boolean blockChanges = false;
    protected RecordSet recordSet = null;

    protected JButton saveBtn = null;
    protected JButton deleteRowsBtn = null;
    protected JButton clearCellsBtn = null;
    protected JButton addRowsBtn = null;
    protected JButton carryForwardBtn = null;
    protected JButton toggleImageFrameBtn = null;
    protected JButton showMapBtn = null;
    protected JButton controlPropsBtn = null;
    protected JButton exportKmlBtn = null;
    protected JButton geoRefToolBtn = null;
    protected JButton convertGeoRefFormatBtn = null;
    protected JButton exportExcelCsvBtn = null;
    protected JButton uploadDatasetBtn = null;
    protected JButton showHideUploadToolBtn = null;
    protected UploadToolPanel uploadToolPanel = null;
    protected JButton importImagesBtn = null;

    protected DropDownButtonStateful ssFormSwitcher = null;
    protected List<JButton> selectionSensitiveButtons = new Vector<JButton>();

    protected int currentRow = 0;
    protected FormPaneWrapper formPane;
    protected ResultSetController resultsetController;

    protected CardLayout cardLayout = null;
    protected JPanel mainPanel;
    protected PanelType currentPanelType = PanelType.Spreadsheet;

    protected JSplitPane uploadPane = null;

    protected JPanel controllerPane;
    protected CardLayout cpCardLayout = null;

    protected ImageFrame imageFrame = null;
    protected ImageImportFrame imageImportFrame = null;
    protected boolean imageFrameWasShowing = false;
    protected ListSelectionListener workbenchRowChangeListener = null;

    protected JFrame mapFrame = null;
    protected JLabel mapImageLabel = null;

    protected WindowListener minMaxWindowListener = null;

    protected CustomDialog geoRefConvertDlg = null;

    private static class WorkbenchPluginMap extends HashMap<Class<?>, WorkBenchPluginIFace> {
    }

    protected WorkbenchPluginMap workBenchPlugins = new WorkbenchPluginMap();
    protected Vector<JComponent> workBenchPluginSSBtns = new Vector<JComponent>();
    protected Vector<JComponent> workBenchPluginFormBtns = new Vector<JComponent>();

    /**
     * The currently active Uploader. 
     * static to help prevent multiple simultaneous uploads.
     */
    protected static Uploader datasetUploader = null;
    protected WorkbenchValidator workbenchValidator = null;
    protected boolean doIncrementalValidation = false;
    protected boolean doIncrementalMatching = false;
    protected UniquenessChecker catNumChecker = null;
    protected int catNumCol = -1;
    protected AtomicInteger invalidCellCount = new AtomicInteger(0);
    protected AtomicInteger unmatchedCellCount = new AtomicInteger(0);
    protected CellRenderingAttributes cellRenderAtts = new CellRenderingAttributes();
    protected boolean restoreUploadToolPanel = false;

    //Single thread executor to ensure that rows are not validated concurrently as a result of batch operations
    //protected final ExecutorService validationExecutor         = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
    protected final Queue<ValidationWorker> validationWorkerQueue = new LinkedList<ValidationWorker>();

    // XXX PREF
    protected int mapSize = 500;

    protected boolean isReadOnly;

    protected AtomicInteger shutdownLock = new AtomicInteger(0);
    private TableColumnExt sgrColExt;

    /**
     * Constructs the pane for the spreadsheet.
     * 
     * @param name the name of the pane
     * @param task the owning task
     * @param workbench the workbench to be edited
     * @param showImageView shows image window when first showing the window
     */
    public WorkbenchPaneSS(final String name, final Taskable task, final Workbench workbenchArg,
            final boolean showImageView, final boolean isReadOnly) throws Exception {
        super(name, task);

        removeAll();

        if (workbenchArg == null) {
            return;
        }
        this.workbench = workbenchArg;

        this.isReadOnly = isReadOnly;

        headers.addAll(workbench.getWorkbenchTemplate().getWorkbenchTemplateMappingItems());
        Collections.sort(headers);

        boolean hasOneOrMoreImages = false;
        // pre load all the data
        for (WorkbenchRow wbRow : workbench.getWorkbenchRows()) {
            for (WorkbenchDataItem wbdi : wbRow.getWorkbenchDataItems()) {
                wbdi.getCellData();
            }

            if (wbRow.getWorkbenchRowImages() != null && wbRow.getWorkbenchRowImages().size() > 0) {
                hasOneOrMoreImages = true;
            }
        }

        model = new GridTableModel(this);
        spreadSheet = new WorkbenchSpreadSheet(model, this);
        spreadSheet.setReadOnly(isReadOnly);
        model.setSpreadSheet(spreadSheet);

        Highlighter simpleStriping = HighlighterFactory.createSimpleStriping();
        GridCellHighlighter hl = new GridCellHighlighter(
                new GridCellPredicate(GridCellPredicate.AnyPredicate, null));
        Short[] errs = { WorkbenchDataItem.VAL_ERROR, WorkbenchDataItem.VAL_ERROR_EDIT };
        ColorHighlighter errColorHighlighter = new ColorHighlighter(
                new GridCellPredicate(GridCellPredicate.ValidationPredicate, errs),
                CellRenderingAttributes.errorBackground, null);
        Short[] newdata = { WorkbenchDataItem.VAL_NEW_DATA };
        ColorHighlighter noDataHighlighter = new ColorHighlighter(
                new GridCellPredicate(GridCellPredicate.MatchingPredicate, newdata),
                CellRenderingAttributes.newDataBackground, null);
        Short[] multimatch = { WorkbenchDataItem.VAL_MULTIPLE_MATCH };
        ColorHighlighter multiMatchHighlighter = new ColorHighlighter(
                new GridCellPredicate(GridCellPredicate.MatchingPredicate, multimatch),
                CellRenderingAttributes.multipleMatchBackground, null);

        spreadSheet.setHighlighters(simpleStriping, hl, errColorHighlighter, noDataHighlighter,
                multiMatchHighlighter);

        //add key mappings for cut, copy, paste
        //XXX Note: these are shortcuts directly to the SpreadSheet cut,copy,paste methods, NOT to the Specify edit menu.
        addRecordKeyMappings(spreadSheet, KeyEvent.VK_C, "Copy", new AbstractAction() {
            public void actionPerformed(ActionEvent ae) {
                SwingUtilities.invokeLater(new Runnable() {

                    /* (non-Javadoc)
                     * @see java.lang.Runnable#run()
                     */
                    @Override
                    public void run() {
                        spreadSheet.cutOrCopy(false);
                    }
                });
            }
        }, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
        addRecordKeyMappings(spreadSheet, KeyEvent.VK_X, "Cut", new AbstractAction() {
            public void actionPerformed(ActionEvent ae) {
                SwingUtilities.invokeLater(new Runnable() {

                    /* (non-Javadoc)
                     * @see java.lang.Runnable#run()
                     */
                    @Override
                    public void run() {
                        spreadSheet.cutOrCopy(true);
                    }
                });
            }
        }, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
        addRecordKeyMappings(spreadSheet, KeyEvent.VK_V, "Paste", new AbstractAction() {
            public void actionPerformed(ActionEvent ae) {
                SwingUtilities.invokeLater(new Runnable() {

                    /* (non-Javadoc)
                     * @see java.lang.Runnable#run()
                     */
                    @Override
                    public void run() {
                        spreadSheet.paste();
                    }
                });
            }
        }, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());

        findPanel = spreadSheet.getFindReplacePanel();
        UIRegistry.getLaunchFindReplaceAction().setSearchReplacePanel(findPanel);

        spreadSheet.setShowGrid(true);
        JTableHeader header = spreadSheet.getTableHeader();
        header.addMouseListener(new ColumnHeaderListener());
        header.setReorderingAllowed(false); // Turn Off column dragging

        // Put the model in image mode, and never change it.
        // Now we're showing/hiding the image column using JXTable's column hiding features.
        model.setInImageMode(true);
        int imageColIndex = model.getColumnCount() - 1;
        imageColExt = spreadSheet.getColumnExt(imageColIndex);
        imageColExt.setVisible(false);

        int sgrColIndex = model.getSgrHeading().getViewOrder();
        sgrColExt = spreadSheet.getColumnExt(sgrColIndex);
        sgrColExt.setComparator(((WorkbenchSpreadSheet) spreadSheet).new NumericColumnComparator());

        int cmpIdx = 0;
        for (Comparator<String> cmp : ((WorkbenchSpreadSheet) spreadSheet).getComparators()) {
            if (cmp != null) {
                spreadSheet.getColumnExt(cmpIdx++).setComparator(cmp);
            }
        }

        // Start off with the SGR score column hidden
        showHideSgrCol(false);

        model.addTableModelListener(new TableModelListener() {
            public void tableChanged(TableModelEvent e) {
                setChanged(true);
            }
        });

        spreadSheet.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                UIRegistry.enableCutCopyPaste(true);
                UIRegistry.enableFind(findPanel, true);
            }

            @Override
            public void focusLost(FocusEvent e) {
                UIRegistry.enableCutCopyPaste(true);
                UIRegistry.enableFind(findPanel, true);
            }
        });

        if (isReadOnly) {
            saveBtn = null;
        } else {
            saveBtn = createButton(getResourceString("SAVE"));
            saveBtn.setToolTipText(
                    String.format(getResourceString("WB_SAVE_DATASET_TT"), new Object[] { workbench.getName() }));
            saveBtn.setEnabled(false);
            saveBtn.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent ae) {
                    UsageTracker.incrUsageCount("WB.SaveDataSet");

                    UIRegistry.writeSimpleGlassPaneMsg(
                            String.format(getResourceString("WB_SAVING"), new Object[] { workbench.getName() }),
                            WorkbenchTask.GLASSPANE_FONT_SIZE);
                    UIRegistry.getStatusBar().setIndeterminate(workbench.getName(), true);
                    final SwingWorker worker = new SwingWorker() {
                        @SuppressWarnings("synthetic-access")
                        @Override
                        public Object construct() {
                            try {
                                saveObject();

                            } catch (Exception ex) {
                                UsageTracker.incrHandledUsageCount();
                                edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(WorkbenchPaneSS.class,
                                        ex);
                                log.error(ex);
                                return ex;
                            }
                            return null;
                        }

                        // Runs on the event-dispatching thread.
                        @Override
                        public void finished() {
                            Object retVal = get();
                            if (retVal != null && retVal instanceof Exception) {
                                Exception ex = (Exception) retVal;
                                UIRegistry.getStatusBar().setErrorMessage(getResourceString("WB_ERROR_SAVING"), ex);
                            }

                            UIRegistry.clearSimpleGlassPaneMsg();
                            UIRegistry.getStatusBar().setProgressDone(workbench.getName());
                        }
                    };
                    worker.start();

                }
            });
        }

        Action delAction = addRecordKeyMappings(spreadSheet, KeyEvent.VK_F3, "DelRow", new AbstractAction() {
            public void actionPerformed(ActionEvent ae) {
                if (validationWorkerQueue.peek() == null) {
                    deleteRows();
                }
            }
        }, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());

        if (isReadOnly) {
            deleteRowsBtn = null;
        } else {
            deleteRowsBtn = createIconBtn("DelRec", "WB_DELETE_ROW", delAction);
            selectionSensitiveButtons.add(deleteRowsBtn);
            spreadSheet.setDeleteAction(delAction);
        }

        //XXX Using the wb ID in the prefname to do pref setting per wb, may result in a bloated prefs file?? 
        doIncrementalValidation = AppPreferences.getLocalPrefs()
                .getBoolean(wbAutoValidatePrefName + "." + workbench.getId(), true);
        doIncrementalMatching = AppPreferences.getLocalPrefs()
                .getBoolean(wbAutoMatchPrefName + "." + workbench.getId(), false);

        if (isReadOnly) {
            clearCellsBtn = null;
        } else {
            clearCellsBtn = createIconBtn("Eraser", "WB_CLEAR_CELLS", new ActionListener() {
                public void actionPerformed(ActionEvent ae) {
                    spreadSheet.clearSorter();

                    if (spreadSheet.getCellEditor() != null) {
                        spreadSheet.getCellEditor().stopCellEditing();
                    }
                    int[] rows = spreadSheet.getSelectedRowModelIndexes();
                    int[] cols = spreadSheet.getSelectedColumnModelIndexes();
                    model.clearCells(rows, cols);
                }
            });
            selectionSensitiveButtons.add(clearCellsBtn);
        }

        Action addAction = addRecordKeyMappings(spreadSheet, KeyEvent.VK_N, "AddRow", new AbstractAction() {
            public void actionPerformed(ActionEvent ae) {
                if (workbench.getWorkbenchRows().size() < WorkbenchTask.MAX_ROWS) {
                    if (validationWorkerQueue.peek() == null) {
                        addRowAfter();
                    }
                }
            }
        }, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());

        if (isReadOnly) {
            addRowsBtn = null;
        } else {
            addRowsBtn = createIconBtn("AddRec", "WB_ADD_ROW", addAction);
            addRowsBtn.setEnabled(true);
            addAction.setEnabled(true);
        }

        if (isReadOnly) {
            carryForwardBtn = null;
        } else {
            carryForwardBtn = createIconBtn("CarryForward20x20", IconManager.IconSize.NonStd, "WB_CARRYFORWARD",
                    false, new ActionListener() {
                        public void actionPerformed(ActionEvent ae) {
                            UsageTracker.getUsageCount("WBCarryForward");

                            configCarryFoward();
                        }
                    });
            carryForwardBtn.setEnabled(true);
        }

        toggleImageFrameBtn = createIconBtn("CardImage", IconManager.IconSize.NonStd, "WB_SHOW_IMG_WIN", false,
                new ActionListener() {
                    public void actionPerformed(ActionEvent ae) {
                        toggleImageFrameVisible();
                    }
                });
        toggleImageFrameBtn.setEnabled(true);

        importImagesBtn = createIconBtn("CardImage", IconManager.IconSize.NonStd, "WB_SHOW_IMG_WIN", false,
                new ActionListener() {
                    public void actionPerformed(ActionEvent ae) {
                        toggleImportImageFrameVisible();
                    }
                });
        importImagesBtn.setEnabled(true);

        /*showMapBtn = createIconBtn("ShowMap", IconManager.IconSize.NonStd, "WB_SHOW_MAP", false, new ActionListener()
        {
        public void actionPerformed(ActionEvent ae)
        {
            showMapOfSelectedRecords();
        }
        });*/
        // enable or disable along with Google Earth and Geo Ref Convert buttons

        if (isReadOnly) {
            exportKmlBtn = null;
        } else {
            exportKmlBtn = createIconBtn("GoogleEarth", IconManager.IconSize.NonStd, "WB_SHOW_IN_GOOGLE_EARTH",
                    false, new ActionListener() {
                        public void actionPerformed(ActionEvent ae) {
                            SwingUtilities.invokeLater(new Runnable() {
                                public void run() {
                                    showRecordsInGoogleEarth();
                                }
                            });
                        }
                    });
        }

        // 

        readRegisteries();

        // enable or disable along with Show Map and Geo Ref Convert buttons

        if (isReadOnly) {
            geoRefToolBtn = null;
        } else {
            AppPreferences remotePrefs = AppPreferences.getRemote();
            final String tool = remotePrefs.get("georef_tool", "geolocate");
            String iconName = "GEOLocate20"; //tool.equalsIgnoreCase("geolocate") ? "GeoLocate" : "BioGeoMancer";
            String toolTip = tool.equalsIgnoreCase("geolocate") ? "WB_DO_GEOLOCATE_LOOKUP"
                    : "WB_DO_BIOGEOMANCER_LOOKUP";
            geoRefToolBtn = createIconBtn(iconName, IconManager.IconSize.NonStd, toolTip, false,
                    new ActionListener() {
                        public void actionPerformed(ActionEvent ae) {
                            spreadSheet.clearSorter();

                            if (tool.equalsIgnoreCase("geolocate")) {
                                doGeoRef(new edu.ku.brc.services.geolocate.prototype.GeoCoordGeoLocateProvider(),
                                        "WB.GeoLocateRows");
                            } else {
                                doGeoRef(new GeoCoordBGMProvider(), "WB.BioGeomancerRows");
                            }
                        }
                    });
            // only enable it if the workbench has the proper columns in it
            String[] missingColumnsForBG = getMissingButRequiredColumnsForGeoRefTool(tool);
            if (missingColumnsForBG.length > 0) {
                geoRefToolBtn.setEnabled(false);
                String ttText = "<p>" + getResourceString("WB_ADDITIONAL_FIELDS_REQD") + ":<ul>";
                for (String reqdField : missingColumnsForBG) {
                    ttText += "<li>" + reqdField + "</li>";
                }
                ttText += "</ul>";
                String origTT = geoRefToolBtn.getToolTipText();
                geoRefToolBtn.setToolTipText("<html>" + origTT + ttText);
            } else {
                geoRefToolBtn.setEnabled(true);
            }
        }

        if (isReadOnly) {
            convertGeoRefFormatBtn = null;
        } else {
            convertGeoRefFormatBtn = createIconBtn("ConvertGeoRef", IconManager.IconSize.NonStd,
                    "WB_CONVERT_GEO_FORMAT", false, new ActionListener() {
                        public void actionPerformed(ActionEvent ae) {
                            showGeoRefConvertDialog();
                        }
                    });

            // now enable/disable the geo ref related buttons
            String[] missingGeoRefFields = getMissingGeoRefLatLonFields();
            if (missingGeoRefFields.length > 0) {
                convertGeoRefFormatBtn.setEnabled(false);
                exportKmlBtn.setEnabled(false);
                //showMapBtn.setEnabled(false);

                String ttText = "<p>" + getResourceString("WB_ADDITIONAL_FIELDS_REQD") + ":<ul>";
                for (String reqdField : missingGeoRefFields) {
                    ttText += "<li>" + reqdField + "</li>";
                }
                ttText += "</ul>";
                String origTT1 = convertGeoRefFormatBtn.getToolTipText();
                convertGeoRefFormatBtn.setToolTipText("<html>" + origTT1 + ttText);
                String origTT2 = exportKmlBtn.getToolTipText();
                exportKmlBtn.setToolTipText("<html>" + origTT2 + ttText);
                //String origTT3 = showMapBtn.getToolTipText();
                //showMapBtn.setToolTipText("<html>" + origTT3 + ttText);
            } else {
                convertGeoRefFormatBtn.setEnabled(true);
                exportKmlBtn.setEnabled(true);
                //showMapBtn.setEnabled(true);
            }
        }

        if (AppContextMgr.isSecurityOn() && !task.getPermissions().canModify()) {
            exportExcelCsvBtn = null;
        } else {
            exportExcelCsvBtn = createIconBtn("Export", IconManager.IconSize.NonStd, "WB_EXPORT_DATA", false,
                    new ActionListener() {
                        public void actionPerformed(ActionEvent ae) {
                            doExcelCsvExport();
                        }
                    });
            exportExcelCsvBtn.setEnabled(true);
        }

        uploadDatasetBtn = createIconBtn("Upload", IconManager.IconSize.Std24, "WB_UPLOAD_DATA", false,
                new ActionListener() {
                    public void actionPerformed(ActionEvent ae) {
                        doDatasetUpload();
                    }
                });
        uploadDatasetBtn.setVisible(isUploadPermitted() && !UIRegistry.isMobile());
        uploadDatasetBtn.setEnabled(canUpload());
        if (!uploadDatasetBtn.isEnabled()) {
            uploadDatasetBtn.setToolTipText(getResourceString("WB_UPLOAD_IN_PROGRESS"));
        }

        // listen to selection changes to enable/disable certain buttons
        spreadSheet.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent e) {
                if (!e.getValueIsAdjusting()) {
                    JStatusBar statusBar = UIRegistry.getStatusBar();
                    statusBar.setText("");

                    currentRow = spreadSheet.getSelectedRow();
                    updateBtnUI();
                }
            }
        });

        for (int c = 0; c < spreadSheet.getTableHeader().getColumnModel().getColumnCount(); c++) {
            // TableColumn column =
            // spreadSheet.getTableHeader().getColumnModel().getColumn(spreadSheet.getTableHeader().getColumnModel().getColumnCount()-1);
            TableColumn column = spreadSheet.getTableHeader().getColumnModel().getColumn(c);
            column.setCellRenderer(new WbCellRenderer());
        }

        // setup the JFrame to show images attached to WorkbenchRows
        imageFrame = new ImageFrame(mapSize, this, this.workbench, task, isReadOnly);

        // setup the JFrame to show images attached to WorkbenchRows
        imageImportFrame = new ImageImportFrame(this, this.workbench);

        setupWorkbenchRowChangeListener();

        // setup window minimizing/maximizing listener
        JFrame topFrame = (JFrame) UIRegistry.getTopWindow();
        minMaxWindowListener = new WindowAdapter() {
            @Override
            public void windowDeiconified(WindowEvent e) {
                if (imageFrame != null && imageFrame.isVisible()) {
                    imageFrame.setExtendedState(Frame.NORMAL);
                }
                if (mapFrame != null && mapFrame.isVisible()) {
                    mapFrame.setExtendedState(Frame.NORMAL);
                }
            }

            @Override
            public void windowIconified(WindowEvent e) {
                if (imageFrame != null && imageFrame.isVisible()) {
                    imageFrame.setExtendedState(Frame.ICONIFIED);
                }
                if (mapFrame != null && mapFrame.isVisible()) {
                    mapFrame.setExtendedState(Frame.ICONIFIED);
                }
            }
        };
        topFrame.addWindowListener(minMaxWindowListener);

        if (!isReadOnly) {
            showHideUploadToolBtn = createIconBtn("ValidateWB", IconManager.IconSize.NonStd,
                    "WB_HIDE_UPLOADTOOLPANEL", false, new ActionListener() {
                        public void actionPerformed(ActionEvent ae) {
                            if (uploadToolPanel.isExpanded()) {
                                hideUploadToolPanel();
                                showHideUploadToolBtn.setToolTipText(getResourceString("WB_SHOW_UPLOADTOOLPANEL"));
                            } else {
                                showUploadToolPanel();
                                showHideUploadToolBtn.setToolTipText(getResourceString("WB_HIDE_UPLOADTOOLPANEL"));
                            }
                        }
                    });
            showHideUploadToolBtn.setEnabled(true);
        }

        // setup the mapping features
        mapFrame = new JFrame();
        mapFrame.setIconImage(IconManager.getImage("AppIcon").getImage());
        mapFrame.setTitle(getResourceString("WB_GEO_REF_DATA_MAP"));
        mapImageLabel = createLabel("");
        mapImageLabel.setSize(500, 500);
        mapFrame.add(mapImageLabel);
        mapFrame.setSize(500, 500);

        // start putting together the visible UI
        CellConstraints cc = new CellConstraints();

        JComponent[] compsArray = { addRowsBtn, deleteRowsBtn, clearCellsBtn, /*showMapBtn,*/ exportKmlBtn,
                geoRefToolBtn, convertGeoRefFormatBtn, exportExcelCsvBtn, uploadDatasetBtn, showHideUploadToolBtn };
        Vector<JComponent> availableComps = new Vector<JComponent>(
                compsArray.length + workBenchPluginSSBtns.size());
        for (JComponent c : compsArray) {
            if (c != null) {
                availableComps.add(c);
            }
        }
        for (JComponent c : workBenchPluginSSBtns) {
            availableComps.add(c);
        }

        PanelBuilder spreadSheetControlBar = new PanelBuilder(new FormLayout(
                "f:p:g,4px," + createDuplicateJGoodiesDef("p", "4px", availableComps.size()) + ",4px,", "c:p:g"));

        int x = 3;
        for (JComponent c : availableComps) {
            spreadSheetControlBar.add(c, cc.xy(x, 1));
            x += 2;
        }

        int h = 0;
        Vector<WorkbenchTemplateMappingItem> headers = new Vector<WorkbenchTemplateMappingItem>();
        headers.addAll(workbench.getWorkbenchTemplate().getWorkbenchTemplateMappingItems());
        Collections.sort(headers);
        for (WorkbenchTemplateMappingItem mi : headers) {
            //using the workbench data model table. Not the actual specify table the column is mapped to.
            //This MIGHT be less confusing
            //System.out.println("setting header renderer for " + mi.getTableName() + "." + mi.getFieldName()); 
            spreadSheet.getColumnModel().getColumn(h++)
                    .setHeaderRenderer(new WbTableHeaderRenderer(mi.getTableName()));
        }

        // NOTE: This needs to be done after the creation of the saveBtn. And after the creation of the header renderes.
        initColumnSizes(spreadSheet, saveBtn);

        // Create the Form Pane  -- needs to be done after initColumnSizes - which also sets cell editors for collumns
        if (task instanceof SGRTask) {
            formPane = new SGRFormPane(this, workbench, isReadOnly);
        } else {
            formPane = new FormPane(this, workbench, isReadOnly);
        }

        // This panel contains just the ResultSetContoller, it's needed so the RSC gets centered
        PanelBuilder rsPanel = new PanelBuilder(new FormLayout("c:p:g", "c:p:g"));
        FormValidator dummy = new FormValidator(null);
        dummy.setEnabled(true);
        resultsetController = new ResultSetController(dummy, !isReadOnly, !isReadOnly, false,
                getResourceString("Record"), model.getRowCount(), true);
        resultsetController.addListener(formPane);
        if (!isReadOnly) {
            resultsetController.getDelRecBtn().addActionListener(delAction);
        }
        //        else
        //        {
        //            resultsetController.getDelRecBtn().setVisible(false);
        //        }
        rsPanel.add(resultsetController.getPanel(), cc.xy(1, 1));

        // This panel is a single row containing the ResultSetContoller and the other controls for the Form Panel
        String colspec = "f:p:g, p, f:p:g, p";
        for (int i = 0; i < workBenchPluginFormBtns.size(); i++) {
            colspec = colspec + ", f:p, p";
        }

        PanelBuilder resultSetPanel = new PanelBuilder(new FormLayout(colspec, "c:p:g"));
        // Now put the two panel into the single row panel
        resultSetPanel.add(rsPanel.getPanel(), cc.xy(2, 1));
        if (!isReadOnly) {
            resultSetPanel.add(formPane.getControlPropsBtn(), cc.xy(4, 1));
        }
        int ccx = 6;
        for (JComponent c : workBenchPluginFormBtns) {
            resultSetPanel.add(c, cc.xy(ccx, 1));
            ccx += 2;
        }

        // Create the main panel that uses card layout for the form and spreasheet
        mainPanel = new JPanel(cardLayout = new CardLayout());

        // Add the Form and Spreadsheet to the CardLayout
        mainPanel.add(spreadSheet.getScrollPane(), PanelType.Spreadsheet.toString());
        mainPanel.add(formPane.getPane(), PanelType.Form.toString());

        // The controllerPane is a CardLayout that switches between the Spreadsheet control bar and the Form Control Bar
        controllerPane = new JPanel(cpCardLayout = new CardLayout());
        controllerPane.add(spreadSheetControlBar.getPanel(), PanelType.Spreadsheet.toString());
        controllerPane.add(resultSetPanel.getPanel(), PanelType.Form.toString());

        JLabel sep1 = new JLabel(IconManager.getIcon("Separator"));
        JLabel sep2 = new JLabel(IconManager.getIcon("Separator"));
        ssFormSwitcher = createSwitcher();

        // This works
        setLayout(new BorderLayout());

        boolean doDnDImages = AppPreferences.getLocalPrefs().getBoolean("WB_DND_IMAGES", false);
        JComponent[] ctrlCompArray1 = { importImagesBtn, toggleImageFrameBtn, carryForwardBtn, sep1, saveBtn, sep2,
                ssFormSwitcher };
        JComponent[] ctrlCompArray2 = { toggleImageFrameBtn, carryForwardBtn, sep1, saveBtn, sep2, ssFormSwitcher };
        JComponent[] ctrlCompArray = doDnDImages ? ctrlCompArray1 : ctrlCompArray2;

        Vector<Pair<JComponent, Integer>> ctrlComps = new Vector<Pair<JComponent, Integer>>();
        for (JComponent c : ctrlCompArray) {
            ctrlComps.add(new Pair<JComponent, Integer>(c, null));
        }

        String layoutStr = "";
        int compCount = 0;
        int col = 1;
        int pos = 0;
        for (Pair<JComponent, Integer> c : ctrlComps) {
            JComponent comp = c.getFirst();
            if (comp != null) {
                boolean addComp = !(comp == sep1 || comp == sep2) || compCount > 0;
                if (!addComp) {
                    c.setFirst(null);
                } else {
                    if (!StringUtils.isEmpty(layoutStr)) {
                        layoutStr += ",";
                        col++;
                        if (pos < ctrlComps.size() - 1) {
                            //this works because we know ssFormSwitcher is last and always non-null.
                            layoutStr += "6px,";
                            col++;
                        }
                    }
                    c.setSecond(col);
                    if (comp == sep1 || comp == sep2) {
                        layoutStr += "6px";
                        compCount = 0;
                    } else {
                        layoutStr += "p";
                        compCount++;
                    }
                }
            }
            pos++;
        }
        PanelBuilder ctrlBtns = new PanelBuilder(new FormLayout(layoutStr, "c:p:g"));
        for (Pair<JComponent, Integer> c : ctrlComps) {
            if (c.getFirst() != null) {
                ctrlBtns.add(c.getFirst(), cc.xy(c.getSecond(), 1));
            }
        }

        add(mainPanel, BorderLayout.CENTER);

        FormLayout formLayout = new FormLayout("f:p:g,4px,p", "2px,f:p:g,p:g,p:g");
        PanelBuilder builder = new PanelBuilder(formLayout);

        builder.add(controllerPane, cc.xy(1, 2));
        builder.add(ctrlBtns.getPanel(), cc.xy(3, 2));

        if (!isReadOnly) {
            uploadToolPanel = new UploadToolPanel(this, UploadToolPanel.EXPANDED);
            uploadToolPanel.createUI();
            //            showHideUploadToolBtn = createIconBtn("ValidateWB", IconManager.IconSize.NonStd, "WB_HIDE_UPLOADTOOLPANEL", false, new ActionListener()
            //            {
            //                public void actionPerformed(ActionEvent ae)
            //                {
            //                    if (uploadToolPanel.isExpanded())
            //                    {
            //                       hideUploadToolPanel();
            //                       showHideUploadToolBtn.setToolTipText(getResourceString("WB_SHOW_UPLOADTOOLPANEL"));
            //                    } else
            //                    {
            //                       showUploadToolPanel();
            //                       showHideUploadToolBtn.setToolTipText(getResourceString("WB_HIDE_UPLOADTOOLPANEL"));
            //                   }
            //                }
            //            });
            //            showHideUploadToolBtn.setEnabled(true);
        }

        builder.add(uploadToolPanel, cc.xywh(1, 3, 3, 1));
        builder.add(findPanel, cc.xywh(1, 4, 3, 1));

        add(builder.getPanel(), BorderLayout.SOUTH);

        resultsetController.addListener(new ResultSetControllerListener() {
            public boolean indexAboutToChange(int oldIndex, int newIndex) {
                return true;
            }

            public void indexChanged(int newIndex) {
                if (imageFrame != null) {
                    if (newIndex > -1) {
                        int index = spreadSheet.convertRowIndexToModel(newIndex);
                        imageFrame.setRow(workbench.getRow(index));
                    } else {
                        imageFrame.setRow(null);
                    }
                }
            }

            public void newRecordAdded() {
                // do nothing
            }
        });
        //compareSchemas();
        if (getIncremental() && workbenchValidator == null) {
            buildValidator();
        }

        //        int c = 0;
        //        Vector<WorkbenchTemplateMappingItem> headers = new Vector<WorkbenchTemplateMappingItem>();
        //        headers.addAll(workbench.getWorkbenchTemplate().getWorkbenchTemplateMappingItems());
        //        Collections.sort(headers);
        //        for (WorkbenchTemplateMappingItem mi : headers)
        //        {
        //           //using the workbench data model table. Not the actual specify table the column is mapped to.
        //           //This MIGHT be less confusing
        //           //System.out.println("setting header renderer for " + mi.getTableName() + "." + mi.getFieldName()); 
        //           spreadSheet.getColumnModel().getColumn(c++).setHeaderRenderer(new WbTableHeaderRenderer(mi.getTableName()));
        //        }
        //        
        //        // NOTE: This needs to be done after the creation of the saveBtn. And after the creation of the header renderes.
        //        initColumnSizes(spreadSheet, saveBtn);

        // See if we need to make the Image Frame visible
        // Commenting this out for now because it is so annoying.

        if (showImageView || hasOneOrMoreImages) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    toggleImageFrameVisible();

                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            final Frame f = (Frame) UIRegistry.get(UIRegistry.FRAME);
                            f.toFront();
                            f.requestFocus();
                        }
                    });
                }
            });
        }

        ((WorkbenchTask) ContextMgr.getTaskByClass(WorkbenchTask.class)).opening(this);
        ((SGRTask) ContextMgr.getTaskByClass(SGRTask.class)).opening(this);
    }

    /**
     * @return
     */
    public List<UploadField> getAutoAssignableFlds() {
        List<UploadField> result = null;
        if (workbenchValidator == null) {
            buildValidator(true);
        }
        if (workbenchValidator != null) {
            result = workbenchValidator.getUploader().getAutoAssignableFields();
        }
        return result;
    }

    /**
     * @param show
     */
    public void showHideSgrCol(boolean show) {
        sgrColExt.setVisible(show);
    }

    /**
     * 
     */
    public void sgrSort() {
        int sgrColIndex = model.getSgrHeading().getViewOrder();
        spreadSheet.setSortOrder(sgrColIndex, SortOrder.DESCENDING);
    }

    /**
     * @return true if automatch or autovalidate is on
     */
    protected boolean getIncremental() {
        return doIncrementalValidation || doIncrementalMatching;

    }

    protected void showUploadToolPanel() {
        uploadToolPanel.expand();
    }

    protected void hideUploadToolPanel() {
        uploadToolPanel.contract();
    }

    /**
    * @return the doIncrementalValidation
    */
    public boolean isDoIncrementalValidation() {
        return doIncrementalValidation;
    }

    /**
     * turns on incremental validation
     */
    public void turnOnIncrementalValidation() {
        doIncrementalValidation = true;

        if (workbenchValidator == null) {
            buildValidator();
        }
        //XXX If incremental matching is already turned on it would save lots
        //of time if validateAll could be called without matching all.
        validateAll(null);

        AppPreferences.getLocalPrefs().putBoolean(wbAutoValidatePrefName + "." + workbench.getId(),
                doIncrementalValidation);
    }

    /**
     * @return the doIncrementalMatching
     */
    public boolean isDoIncrementalMatching() {
        return doIncrementalMatching;
    }

    /**
     * turns on incremental matching
     */
    public void turnOnIncrementalMatching() {
        doIncrementalMatching = true;

        if (workbenchValidator == null) {
            buildValidator();
        }
        if (workbenchValidator != null) {
            setMatchStatusForUploadTables();
            validateAll(null);

            AppPreferences.getLocalPrefs().putBoolean(wbAutoMatchPrefName + "." + workbench.getId(),
                    doIncrementalMatching);
        }
    }

    /**
     * Set up match status behavior. Currently this just sets showMatchInfo on agents and trees,
     * and turns it off for all other tables. This could be customized but would require
     * some datamodel changes, I think, because using preference might be tricky since the prefs
     * would need to be checked and modified or cleared whenever the wb structure was modified.  
     */
    protected void setMatchStatusForUploadTables() {
        workbenchValidator.getUploader().setDefaultMatchStatus();
    }

    /**
      * Checks the cell for cell editing and stops it.
      */
    public boolean checkCurrentEditState() {
        boolean isOK = true;
        if (currentPanelType == PanelType.Spreadsheet) {
            if (spreadSheet != null && spreadSheet.getCellEditor() != null) {
                int index = spreadSheet.getSelectedRow();
                isOK = spreadSheet.getCellEditor().stopCellEditing();
                if (index >= 0) {
                    spreadSheet.setRowSelectionInterval(index, index);
                }
            }
        } else {
            if (formPane != null) {
                formPane.copyDataFromForm();
            }
        }
        return isOK;
    }

    /**
     * @param isNext
     * @return
     */
    protected Pair<Integer, Integer> getNextCellWithStat(boolean isNext, Set<Short> stats) {
        if (!isNext) {
            //System.out.println("goToInvalidCell prev");
        } else {
            //System.out.println("goToInvalidCell next");
        }

        int startRow = spreadSheet.getSelectedRow() >= 0 ? spreadSheet.getSelectedRow() : 0;
        int startCol = spreadSheet.getSelectedColumn() >= 0 ? spreadSheet.getSelectedColumn() : 0;
        int increment = isNext ? 1 : -1;
        int currentRow = startRow;
        int currentCol = startCol + increment;
        if (currentCol < 0 || currentCol == spreadSheet.getColumnCount()) {
            if (currentCol < 0) {
                currentCol = spreadSheet.getColumnCount() - 1; //XXX what about attachment column?
            } else {
                currentCol = 0;
            }
            currentRow += increment;
            if (currentRow < 0) {
                currentRow = spreadSheet.getRowCount() - 1;
            } else if (currentRow == spreadSheet.getRowCount()) {
                currentRow = 0;
            }
        }
        boolean lastRow = false;
        do {
            Hashtable<Short, WorkbenchDataItem> rowItems = workbench
                    .getRow(spreadSheet.convertRowIndexToModel(currentRow)).getItems();
            do {
                WorkbenchDataItem di = rowItems
                        .get(new Short((short) spreadSheet.convertColumnIndexToModel(currentCol)));
                if (di != null && stats.contains(new Short((short) di.getEditorValidationStatus()))) {
                    return new Pair<Integer, Integer>(currentRow, currentCol);
                }
                currentCol += increment;

            } while (currentCol >= 0 && currentCol < spreadSheet.getColumnCount()
                    && (!lastRow || currentCol != startCol));
            if (currentCol < 0) {
                currentCol = spreadSheet.getColumnCount() - 1; //XXX what about attachment column?
            } else if (currentCol == spreadSheet.getColumnCount()) {
                currentCol = 0;
            }

            if (!lastRow) {
                currentRow += increment;
                if (currentRow < 0) {
                    currentRow = spreadSheet.getRowCount() - 1;
                } else if (currentRow == spreadSheet.getRowCount()) {
                    currentRow = 0;
                }
            }
            lastRow = !lastRow && currentRow == startRow;
        } while (currentRow != startRow || lastRow);
        return null;
    }

    /**
     * @param isNext
     */
    public void goToInvalidCell(boolean isNext) {
        Set<Short> stats = new HashSet<Short>();
        stats.add(WorkbenchDataItem.VAL_ERROR);
        stats.add(WorkbenchDataItem.VAL_ERROR_EDIT);
        Pair<Integer, Integer> invalidCell = getNextCellWithStat(isNext, stats);
        if (invalidCell != null) {
            if (spreadSheet.getCellEditor() != null) {
                spreadSheet.getCellEditor().stopCellEditing();
            }
            int row = invalidCell.getFirst();
            int col = invalidCell.getSecond();
            spreadSheet.getSelectionModel().setSelectionInterval(row, row);
            spreadSheet.getColumnModel().getSelectionModel().setSelectionInterval(col, col);
            spreadSheet.scrollCellToVisible(row, col);
            //spreadSheet.editCellAt(invalidCell.getFirst(), invalidCell.getSecond());
        }
    }

    /**
     * @param isNext
     */
    public void goToUnmatchedCell(boolean isNext) {
        Set<Short> stats = new HashSet<Short>();
        stats.add(WorkbenchDataItem.VAL_MULTIPLE_MATCH);
        stats.add(WorkbenchDataItem.VAL_NEW_DATA);
        Pair<Integer, Integer> invalidCell = getNextCellWithStat(isNext, stats);
        if (invalidCell != null) {
            if (spreadSheet.getCellEditor() != null) {
                spreadSheet.getCellEditor().stopCellEditing();
            }
            spreadSheet.getSelectionModel().setSelectionInterval(invalidCell.getFirst(), invalidCell.getFirst());
            spreadSheet.getColumnModel().getSelectionModel().setSelectionInterval(invalidCell.getSecond(),
                    invalidCell.getSecond());
            spreadSheet.scrollCellToVisible(invalidCell.getFirst(), invalidCell.getSecond());
            //spreadSheet.editCellAt(invalidCell.getFirst(), invalidCell.getSecond());
        }
    }

    /**
     * Update enaabled state of buttons effected by the spreadsheet selection.
     */
    protected void updateBtnUI() {
        boolean enable = spreadSheet.getSelectedRow() > -1;
        for (JButton btn : selectionSensitiveButtons) {
            if (btn != null) {
                btn.setEnabled(enable);
            }
        }
        enable = workbench.getWorkbenchRows().size() < WorkbenchTask.MAX_ROWS;
        if (!isReadOnly) {
            addRowsBtn.setEnabled(enable);
        }
        if (!isReadOnly) {
            resultsetController.getNewRecBtn().setEnabled(enable && !isReadOnly);
        }

        uploadToolPanel.updateBtnUI();
        updateUploadBtnState();
    }

    /**
     * @return number of invalid cells
     */
    public int getInvalidCellCount() {
        return invalidCellCount.get();
    }

    /**
     * @return number of unmatched cells
     */
    public int getUnmatchedCellCount() {
        return unmatchedCellCount.get();
    }

    /**
     * Adds a Key mappings.
     * @param comp comp
     * @param keyCode keyCode
     * @param actionName actionName
     * @param action action 
     * @return the action
     */
    public Action addRecordKeyMappings(final JComponent comp, final int keyCode, final String actionName,
            final Action action, int modifiers) {
        InputMap inputMap = comp.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        ActionMap actionMap = comp.getActionMap();

        inputMap.put(KeyStroke.getKeyStroke(keyCode, modifiers), actionName);
        actionMap.put(actionName, action);

        //UIRegistry.registerAction(actionName, action);
        return action;
    }

    /**
     * Setup the row (or selection) listener for the the Image Window. 
     */
    protected void setupWorkbenchRowChangeListener() {
        workbenchRowChangeListener = new ListSelectionListener() {
            private int previouslySelectedRowIndex = -1;

            @SuppressWarnings("synthetic-access")
            public void valueChanged(ListSelectionEvent e) {
                if (e.getValueIsAdjusting() || !imageFrame.isVisible()) {
                    // ignore this until the user quits changing the selection
                    return;
                }

                if (spreadSheet.getCellEditor() != null) {
                    spreadSheet.getCellEditor().stopCellEditing();
                }

                // check to make sure the selection actually changed
                int newSelectionIndex = spreadSheet.getSelectedRow();
                if (newSelectionIndex != previouslySelectedRowIndex) {
                    previouslySelectedRowIndex = newSelectionIndex;

                    showCardImageForSelectedRow();
                }
            }
        };
    }

    /**
     * Adds a row at the end.
     */
    protected void addRowAtEnd() {
        checkCurrentEditState();
        model.appendRow();

        resultsetController.setLength(model.getRowCount());

        adjustSelectionAfterAdd(model.getRowCount() - 1);

        updateBtnUI();
    }

    /**
     * In form view, adds a row after the current row. 
     * 
     * In spreadsheet view adds getSelectedRowCount() new rows after the last selected row.
     */
    public void addRowAfter() {
        spreadSheet.clearSorter();

        checkCurrentEditState();

        int curSelInx = getCurrentIndexFromFormOrSS();
        int rowsToInsert = 1;
        if (currentPanelType == PanelType.Spreadsheet && spreadSheet.getSelectedRowCount() != 0) {
            int[] sels = spreadSheet.getSelectedRows();
            rowsToInsert += sels.length - 1;
            curSelInx = sels[sels.length - 1];
        }
        for (int r = 0; r < rowsToInsert && curSelInx < WorkbenchTask.MAX_ROWS - 1; r++, curSelInx++)
            model.insertAfterRow(curSelInx);

        int count = model.getRowCount();
        resultsetController.setLength(count);

        adjustSelectionAfterAdd(count == 1 ? 0 : curSelInx);

        updateBtnUI();
    }

    /**
     * Inserts a Row above the selection.
     */
    protected void insertRowAbove() {
        checkCurrentEditState();
        int curSelInx = getCurrentIndexFromFormOrSS();
        model.insertRow(curSelInx);

        resultsetController.setLength(model.getRowCount());

        adjustSelectionAfterAdd(curSelInx);

        updateBtnUI();

    }

    /**
     * Adjusts the selection.
     * @param rowIndex the index to be selected
     */
    protected void adjustSelectionAfterAdd(final int rowIndex) {
        if (currentPanelType == PanelType.Spreadsheet) {
            //log.debug("ss.RowCnt: "+spreadSheet.getRowCount()+" ss.SelRow"+spreadSheet.getSelectedRow()+" model.RowCnt: "+model.getRowCount());
            spreadSheet.scrollToRow(rowIndex);
            spreadSheet.setRowSelectionInterval(rowIndex, rowIndex);
            spreadSheet.setColumnSelectionInterval(0, spreadSheet.getColumnCount() - 1);
            spreadSheet.repaint();
            //log.debug("ss.RowCnt: "+spreadSheet.getRowCount()+" ss.SelRow"+spreadSheet.getSelectedRow()+" model.RowCnt: "+model.getRowCount());

        } else {
            resultsetController.setIndex(rowIndex);
        }
    }

    /**
     * @param rows
     * 
     * Checks for invalid/new columns and adjusts validation data structures
     */
    protected void updateValidationStatusForDelete(final int[] rows) {
        Vector<WorkbenchRow> wbRows = workbench.getWorkbenchRowsAsList();
        int invalids = 0;
        int news = 0;
        for (int r : rows) {
            WorkbenchRow wbRow = wbRows.get(r);
            for (WorkbenchDataItem wbdi : wbRow.getWorkbenchDataItems()) {
                short status = (short) wbdi.getEditorValidationStatus();
                if (status == WorkbenchDataItem.VAL_ERROR || status == WorkbenchDataItem.VAL_ERROR_EDIT) {
                    invalids++;
                } else if (status == WorkbenchDataItem.VAL_MULTIPLE_MATCH
                        || status == WorkbenchDataItem.VAL_NEW_DATA) {
                    news++;
                }
            }
        }
        if (invalids > 0) {
            invalidCellCount.set(Math.max(0, invalidCellCount.get() - invalids));
        }
        if (news > 0) {
            unmatchedCellCount.set(Math.max(0, unmatchedCellCount.get() - news));
        }
    }

    /**
     * Deletes the Selected Rows. 
     */
    protected void deleteRows() {
        spreadSheet.clearSorter();

        checkCurrentEditState();
        int[] rows;
        if (currentPanelType == PanelType.Spreadsheet) {
            rows = spreadSheet.getSelectedRowModelIndexes();
            if (rows.length == 0) {
                return;
            }
        } else {
            rows = new int[1];
            rows[0] = spreadSheet.convertRowIndexToModel(resultsetController.getCurrentIndex());
        }

        int firstRow = rows[0];

        resultsetController.setLength(model.getRowCount() - rows.length);

        //        if (validationWorkerQueue.peek() != null)
        //        {
        //           synchronized(validationWorkerQueue)
        //           {
        //              for (int r : rows)
        //              {
        //                 validationWorkerQueue.peek().rowDeleted(r);
        //              }
        //           }
        //        }

        //Or Just wait until validation is done.
        while (validationWorkerQueue.peek() != null) {
            //System.out.println("waiting for validation workers to finish");
            //sit and wait)
        }

        if (isDoIncremental()) {
            updateValidationStatusForDelete(rows);
        }

        model.deleteRows(rows);

        int rowCount = workbench.getWorkbenchRowsAsList().size();

        if (rowCount == 0) {
            spreadSheet.getSelectionModel().clearSelection();
            addRowAtEnd();
        }

        if (currentPanelType == PanelType.Spreadsheet) {
            if (rowCount > 0) {
                if (firstRow >= rowCount) {
                    spreadSheet.setRowSelectionInterval(rowCount - 1, rowCount - 1);
                } else {
                    spreadSheet.setRowSelectionInterval(firstRow, firstRow);
                }
                spreadSheet.setColumnSelectionInterval(0, spreadSheet.getColumnCount() - 1);
            } else {
                spreadSheet.getSelectionModel().clearSelection();
            }
        } else {
            int currentRow = resultsetController.getCurrentIndex();
            resultsetController.setLength(rowCount);
            if (currentRow >= rowCount) {
                resultsetController.setIndex(rowCount - 1);
            } else {
                resultsetController.setIndex(currentRow);
            }
        }

        updateBtnUI();

    }

    /**
     * @return the resultset controller
     */
    public ResultSetController getResultSetController() {
        return resultsetController;
    }

    /**
     * Tells the model there is new data.
     */
    public void addRowToSpreadSheet() {
        if (spreadSheet != null) {
            spreadSheet.addRow();
            model.fireTableStructureChanged();

            resultsetController.setLength(model.getRowCount());

            setChanged(true);

            updateBtnUI();
        }
    }

    /**
     * Tells the model there is new data.
     */
    public void newImagesAdded() {
        /* if (currentPanelType == PanelType.Form)
         {
        resultsetController.setIndex(model.getRowCount());
         } else
         {
        spreadSheet.setRowSelectionInterval(currentRow, currentRow);
        spreadSheet.setColumnSelectionInterval(0, spreadSheet.getColumnCount()-1);
        spreadSheet.scrollToRow(Math.min(currentRow+4, model.getRowCount()));
            
         }
         */
        if (getIncremental()) {
            validateAll(null);
        }
        adjustSelectionAfterAdd(model.getRowCount() - 1);
    }

    /**
     * Tells the GridModel to update itself. 
     */
    public void gridColumnsUpdated() {
        model.fireTableStructureChanged();

        // the above call results in new TableColumnExt objects for each column, it appears

        // re-get the column extension object
        int imageColIndex = model.getColumnCount() - 1;
        imageColExt = spreadSheet.getColumnExt(imageColIndex);
        imageColExt.setVisible(false);

        // show the column if the image frame is showing
        if (imageFrame.isVisible()) {
            imageColExt.setVisible(true);
        }

        int currentRecord = resultsetController.getCurrentIndex();
        spreadSheet.setRowSelectionInterval(currentRecord, currentRecord);
    }

    /**
     * Show image for a selected row. 
     */
    public void refreshImagesForSelectedRow() {
        if (imageFrame != null) {
            imageFrame.refreshImages();
        }
    }

    /**
     * Show image for a selected row. 
     */
    public void showCardImageForSelectedRow() {
        int selectedIndex = getCurrentIndex();
        if (selectedIndex == -1) {
            // no selection
            log.debug("No selection, so removing the card image");
            imageFrame.setRow(null);
            return;
        }

        log.debug("Showing image for row " + selectedIndex);
        int index = spreadSheet.convertRowIndexToModel(selectedIndex);

        WorkbenchRow row = workbench.getWorkbenchRowsAsList().get(index);
        imageFrame.setRow(row);
    }

    /**
     * Returns the Workbench for the Pane.
     * @return the current workbench
     */
    public Workbench getWorkbench() {
        return workbench;
    }

    /**
     * The grid to form switcher.
     * @return The grid to form switcher.
     */
    public DropDownButtonStateful createSwitcher() {
        Vector<DropDownMenuInfo> menuItems = new Vector<DropDownMenuInfo>();
        menuItems.add(new DropDownMenuInfo(getResourceString("Form"),
                IconManager.getImage("EditForm20", IconManager.IconSize.NonStd),
                getResourceString("WB_SHOW_GRID_VIEW")));
        menuItems.add(new DropDownMenuInfo(getResourceString("Grid"),
                IconManager.getImage("Spreadsheet20", IconManager.IconSize.NonStd),
                getResourceString("WB_SHOW_FORM_VIEW")));
        final DropDownButtonStateful switcher = new DropDownButtonStateful(menuItems);
        switcher.setCurrentIndex(1);
        switcher.setToolTipText(getResourceString("SwitchViewsTT"));
        switcher.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
                PanelType panel = switcher.getCurrentIndex() == 1 ? PanelType.Spreadsheet : PanelType.Form;
                showPanel(panel);

                //Until auto-validation is hooked up to form view:
                //               if (panel == PanelType.Spreadsheet && doIncrementalValidation)
                //               {
                //                  validateAll(null);
                //               }

                if (panel == PanelType.Form && getIncremental()) {
                    formPane.updateValidationUI();
                }
            }
        });
        switcher.validate();
        switcher.doLayout();

        return switcher;
    }

    /**
     * Returns the current selected index from a spreadsheet or the form.
     * @return the current selected index from a spreadsheet or the form.
     */
    protected int getCurrentIndex() {
        int selectedInx;
        if (currentPanelType == PanelType.Spreadsheet) {
            selectedInx = spreadSheet.getSelectedRow();
        } else {
            selectedInx = resultsetController.getCurrentIndex();
        }

        //        if (selectedInx > -1)
        //        {
        //            selectedInx = spreadSheet.convertRowIndexToModel(selectedInx);
        //        }

        return selectedInx;
    }

    /**
     * Returns the current selected index from a spreadsheet or the form.
     * @return the current selected index from a spreadsheet or the form.
     */
    protected int getCurrentIndexFromFormOrSS() {
        int selectedInx = getCurrentIndex();

        if (selectedInx == -1) {
            return 0;
        }

        if (spreadSheet.getRowCount() == 0) {
            return 0;
        }
        return selectedInx == -1 ? spreadSheet.getRowCount() : selectedInx;
    }

    public int getSelectedIndexFromView() {
        return spreadSheet.getSelectedRow();
    }

    /**
     * Shows the grid or the form.
     * @param value the panel number
     */
    public void showPanel(final PanelType panelType) {
        checkCurrentEditState();

        currentRow = getCurrentIndexFromFormOrSS();

        currentPanelType = panelType;

        cardLayout.show(mainPanel, currentPanelType.toString());
        cpCardLayout.show(controllerPane, currentPanelType.toString());

        switch (currentPanelType) {
        case Spreadsheet:
            formPane.aboutToShowHide(false);

            // Showing Spreadsheet and hiding form
            if (model.getRowCount() > 0) {
                spreadSheet.setRowSelectionInterval(currentRow, currentRow);
                spreadSheet.setColumnSelectionInterval(0, spreadSheet.getColumnCount() - 1);
                spreadSheet.scrollToRow(Math.min(currentRow + 4, model.getRowCount()));

                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        spreadSheet.requestFocus();
                    }
                });
            }
            // Enable the "Find" action in the Edit menu when a spreadsheet is shown
            UIRegistry.enableFind(findPanel, true);

            //                NavBoxMgr.getInstance().adjustSplitter();
            ssFormSwitcher.setCurrentIndex(1);
            break;
        case Form:
            // About to Show Form and hiding Spreadsheet

            // cancel any editing in a cell in the spreadsheet
            checkCurrentEditState();

            // Tell the form we are switching and that it is about to be shown
            formPane.aboutToShowHide(true);

            // -1 will tell the form to disable
            resultsetController.setIndex(model.getRowCount() > 0 ? currentRow : -1);

            // Hide the find/replace panel when you switch to form view
            findPanel.getHideFindPanelAction().hide();

            // Disable the ctrl-F from the edit menu
            UIRegistry.disableFindFromEditMenu();

            //                if(task instanceof SGRTask)
            //                    NavBoxMgr.getInstance().closeSplitter();

            ssFormSwitcher.setCurrentIndex(0);
            break;
        }

        JComponent[] comps = { addRowsBtn, deleteRowsBtn, clearCellsBtn };
        for (JComponent c : comps) {
            if (c != null) {
                c.setVisible(currentPanelType == PanelType.Spreadsheet);
            }
        }
    }

    /**
     * Shows / Hides the Image Window. 
     */
    public void toggleImageFrameVisible() {
        if (spreadSheet.getCellEditor() != null) {
            spreadSheet.getCellEditor().stopCellEditing();
        }

        boolean isVisible = imageFrame.isVisible();

        setImageFrameVisible(!isVisible);
    }

    /**
     * Shows / Hides the Image Window. 
     */
    public void toggleImportImageFrameVisible() {
        if (spreadSheet.getCellEditor() != null) {
            spreadSheet.getCellEditor().stopCellEditing();
        }

        boolean isVisible = imageImportFrame.isVisible();

        setImageFrameVisible(!isVisible, imageImportFrame, toggleImageFrameBtn, "WB_SHOW_IMG_WIN",
                "WB_HIDE_IMG_WIN", "WorkbenchWorkingWithImages");
    }

    /**
     * Shows / Hides the Image Window. 
     */
    public void setImageFrameVisible(boolean visible) {
        setImageFrameVisible(visible, imageFrame, toggleImageFrameBtn, "WB_SHOW_IMG_WIN", "WB_HIDE_IMG_WIN",
                "WorkbenchWorkingWithImages");
    }

    /**
     * Shows / Hides the Image Window. 
     */
    public void setImageFrameVisible(final boolean visible, final JFrame imgFrame, final JButton toolBtn,
            final String ttHelpVisible, final String ttHelpHidden, final String helpContext) {
        if (visible == imgFrame.isVisible()) {
            return;
        }

        if (spreadSheet.getCellEditor() != null) {
            spreadSheet.getCellEditor().stopCellEditing();
        }

        // and add or remove the ListSelectionListener (to avoid loading images when not visible)
        if (!visible) {
            spreadSheet.setTransferHandler(null);

            // hide the image window

            // turn off alwaysOnTop for Swing repaint reasons (prevents a lock up)
            if (imgFrame.isAlwaysOnTop()) {
                imgFrame.setAlwaysOnTop(false);
            }
            // if the image frame is minimized or iconified, set it to fully visible before doing anything else
            if (imgFrame.getState() == Frame.ICONIFIED) {
                imgFrame.setState(Frame.NORMAL);
            }
            toolBtn.setToolTipText(getResourceString(ttHelpVisible));

            spreadSheet.getSelectionModel().removeListSelectionListener(workbenchRowChangeListener);

            // set the image window and the image column invisible
            imgFrame.setVisible(false);
            imageColExt.setVisible(false);
        } else {
            spreadSheet.setTransferHandler(new WBImageTransferable());

            // show the image window

            UIHelper.positionFrameRelativeToTopFrame(imgFrame);

            // when a user hits the "show image" button, for some reason the selection gets nullified
            // so we'll grab it here, then set it at the end of this method

            toolBtn.setToolTipText(getResourceString(ttHelpHidden));

            spreadSheet.getSelectionModel().addListSelectionListener(workbenchRowChangeListener);
            HelpMgr.setHelpID(this, helpContext);

            // set the image window and the image column visible
            imgFrame.setVisible(true);
            imageColExt.setVisible(true);

            // if the image frame is minimized or iconified, set it to fully visible before doing anything else
            if (imgFrame.getState() == Frame.ICONIFIED) {
                imgFrame.setState(Frame.NORMAL);
            }

            showCardImageForSelectedRow();

            // Without this code below the Image Column doesn't get selected
            // when toggling 
            if (currentPanelType == PanelType.Spreadsheet && currentRow != -1) {
                spreadSheet.setRowSelectionInterval(currentRow, currentRow);
                spreadSheet.setColumnSelectionInterval(0, spreadSheet.getColumnCount() - 1);
                spreadSheet.scrollToRow(Math.min(currentRow + 4, model.getRowCount()));
            }

            TableColumn column = spreadSheet.getTableHeader().getColumnModel()
                    .getColumn(spreadSheet.getTableHeader().getColumnModel().getColumnCount() - 1);
            column.setCellRenderer(new WbCellRenderer());
            spreadSheet.repaint();
        }
    }

    /**
     * Loads a new Image into a WB Row.
     * XXX Note this needs to to be refactored so both the WorkbenchTask and this class use the same image load method.
     * 
     * @param row the row of the new card image
     * @return true if the row was set
     */
    protected boolean loadNewImage(final WorkbenchRow row) {
        ImageFilter imageFilter = new ImageFilter();
        JFileChooser fileChooser = new JFileChooser(
                WorkbenchTask.getDefaultDirPath(WorkbenchTask.IMAGES_FILE_PATH));
        fileChooser.setFileFilter(imageFilter);

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

        localPrefs.put(WorkbenchTask.IMAGES_FILE_PATH, fileChooser.getCurrentDirectory().getAbsolutePath());
        if (userAction == JFileChooser.APPROVE_OPTION) {
            String fullPath = fileChooser.getSelectedFile().getAbsolutePath();
            if (imageFilter.isImageFile(fullPath)) {
                File chosenFile = fileChooser.getSelectedFile();
                row.setCardImage(chosenFile);
                row.setCardImageFullPath(chosenFile.getAbsolutePath());
                if (row.getLoadStatus() != WorkbenchRow.LoadStatus.Successful) {
                    if (!WorkbenchTask.showLoadStatus(row, false)) {
                        return false;
                    }
                }
                return true;
            }

            // turn off alwaysOnTop for Swing repaint reasons (prevents a lock up)
            if (imageFrame != null) {
                imageFrame.setAlwaysOnTop(false);
            }
            JOptionPane.showMessageDialog(UIRegistry.getMostRecentWindow(),
                    String.format(getResourceString("WB_WRONG_IMAGE_TYPE"),
                            new Object[] { FilenameUtils.getExtension(fullPath) }),
                    UIRegistry.getResourceString("WARNING"), JOptionPane.ERROR_MESSAGE);
        }
        return false;
    }

    /**
     * Shows a dialog that enales the user to convert the lat/lon formats. 
     */
    protected void showGeoRefConvertDialog() {
        if (geoRefConvertDlg != null) {
            geoRefConvertDlg.toFront();
            return;
        }

        UsageTracker.incrUsageCount("WB.ShowGeoRefConverter");

        JStatusBar statusBar = UIRegistry.getStatusBar();

        if (!workbench.containsGeoRefData()) {
            statusBar.setErrorMessage(getResourceString("NoGeoRefColumns"));
            return;
        }

        List<String> outputFormats = new Vector<String>();
        String dddddd = getResourceString("DDDDDD");
        String ddmmmm = getResourceString("DDMMMM");
        String ddmmss = getResourceString("DDMMSS");
        String ddddddnsew = getResourceString("DDDDDDNSEW");
        String ddmmmmnsew = getResourceString("DDMMMMNSEW");
        String ddmmssnsew = getResourceString("DDMMSSNSEW");
        outputFormats.add(dddddd);
        outputFormats.add(ddmmmm);
        outputFormats.add(ddmmss);
        outputFormats.add(ddddddnsew);
        outputFormats.add(ddmmmmnsew);
        outputFormats.add(ddmmssnsew);

        final int locTabId = DBTableIdMgr.getInstance().getIdByClassName(Locality.class.getName());
        final int latColIndex = workbench.getColumnIndex(locTabId, "latitude1");
        final int lonColIndex = workbench.getColumnIndex(locTabId, "longitude1");
        final int lat2ColIndex = workbench.getColumnIndex(locTabId, "latitude2");
        final int lon2ColIndex = workbench.getColumnIndex(locTabId, "longitude2");

        JFrame mainFrame = (JFrame) UIRegistry.getTopWindow();

        String title = UIRegistry.getResourceString("GeoRefConv");
        String description = UIRegistry.getResourceString("GeoRefConvDesc");
        final ToggleButtonChooserPanel<String> toggle = new ToggleButtonChooserPanel<String>(outputFormats,
                description, Type.RadioButton);
        final JCheckBox symbolCkBx = UIHelper.createCheckBox(UIRegistry.getResourceString("GEOREF_USE_SYMBOLS"));
        JPanel pane = new JPanel(new BorderLayout());
        pane.add(toggle, BorderLayout.CENTER);
        pane.add(symbolCkBx, BorderLayout.SOUTH);
        geoRefConvertDlg = new CustomDialog(mainFrame, title, false, CustomDialog.OKCANCEL, pane) {

            @Override
            public void setVisible(boolean visible) {
                super.setVisible(visible);

                Dimension prefSize = this.getPreferredSize();
                prefSize.width += 60;
                this.setSize(prefSize);
            }

            @Override
            protected void cancelButtonPressed() {
                geoRefConvertDlg = null;
                super.cancelButtonPressed();
            }

            @Override
            protected void okButtonPressed() {
                checkCurrentEditState();

                // figure out which rows the user is working with
                int[] selection = spreadSheet.getSelectedRowModelIndexes();
                if (selection.length == 0) {
                    // if none are selected, map all of them
                    int rowCnt = spreadSheet.getRowCount();
                    selection = new int[rowCnt];
                    for (int i = 0; i < rowCnt; ++i) {
                        selection[i] = spreadSheet.convertRowIndexToModel(i);
                    }
                }

                // since Arrays.copyOf() isn't in Java SE 5...
                int[] selRows = new int[selection.length];
                for (int i = 0; i < selection.length; ++i) {
                    selRows[i] = selection[i];
                }

                // don't call super.okButtonPressed() b/c it will close the window
                isCancelled = false;
                btnPressed = OK_BTN;
                LatLonConverter.DEGREES_FORMAT degFmt = symbolCkBx.isSelected()
                        ? LatLonConverter.DEGREES_FORMAT.Symbol
                        : LatLonConverter.DEGREES_FORMAT.None;
                Vector<CellPosition> unconverted = new Vector<CellPosition>();
                switch (toggle.getSelectedIndex()) {
                case 0: {
                    unconverted.addAll(convertColumnContents(latColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.D_PLUS_MINUS.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lonColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.D_PLUS_MINUS.name(), LatLonConverter.LATLON.Longitude, degFmt));
                    unconverted.addAll(convertColumnContents(lat2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.D_PLUS_MINUS.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lon2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.D_PLUS_MINUS.name(), LatLonConverter.LATLON.Longitude, degFmt));
                    break;
                }
                case 1: {
                    unconverted.addAll(convertColumnContents(latColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DM_PLUS_MINUS.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lonColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DM_PLUS_MINUS.name(), LatLonConverter.LATLON.Longitude, degFmt));
                    unconverted.addAll(convertColumnContents(lat2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DM_PLUS_MINUS.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lon2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DM_PLUS_MINUS.name(), LatLonConverter.LATLON.Longitude, degFmt));
                    break;
                }
                case 2: {
                    unconverted.addAll(convertColumnContents(latColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DMS_PLUS_MINUS.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lonColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DMS_PLUS_MINUS.name(), LatLonConverter.LATLON.Longitude, degFmt));
                    unconverted.addAll(convertColumnContents(lat2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DMS_PLUS_MINUS.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lon2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DMS_PLUS_MINUS.name(), LatLonConverter.LATLON.Longitude, degFmt));
                    break;
                }
                case 3: {
                    unconverted.addAll(convertColumnContents(latColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.D_NSEW.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lonColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.D_NSEW.name(), LatLonConverter.LATLON.Longitude, degFmt));
                    unconverted.addAll(convertColumnContents(lat2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.D_NSEW.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lon2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.D_NSEW.name(), LatLonConverter.LATLON.Longitude, degFmt));

                    break;
                }
                case 4: {
                    unconverted.addAll(convertColumnContents(latColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DM_NSEW.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lonColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DM_NSEW.name(), LatLonConverter.LATLON.Longitude, degFmt));
                    unconverted.addAll(convertColumnContents(lat2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DM_NSEW.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lon2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DM_NSEW.name(), LatLonConverter.LATLON.Longitude, degFmt));

                    break;
                }
                case 5: {
                    unconverted.addAll(convertColumnContents(latColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DMS_NSEW.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lonColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DMS_NSEW.name(), LatLonConverter.LATLON.Longitude, degFmt));
                    unconverted.addAll(convertColumnContents(lat2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DMS_NSEW.name(), LatLonConverter.LATLON.Latitude, degFmt));
                    unconverted.addAll(convertColumnContents(lon2ColIndex, selRows, new GeoRefConverter(),
                            GeoRefFormat.DMS_NSEW.name(), LatLonConverter.LATLON.Longitude, degFmt));

                    break;
                }
                }
                if (unconverted.size() != 0) {
                    UIRegistry.displayLocalizedStatusBarError("WB_UNCONVERTED_GEOREFS", unconverted.size());
                    final JList<?> unconvertedcells = UIHelper.createList(unconverted);
                    unconvertedcells.addListSelectionListener(new ListSelectionListener() {

                        @Override
                        public void valueChanged(ListSelectionEvent arg0) {
                            CellPosition rowCol = (CellPosition) unconvertedcells.getSelectedValue();
                            spreadSheet.scrollCellToVisible(rowCol.getFirst(), rowCol.getSecond());

                        }

                    });
                    JLabel lbl = UIHelper.createLabel(UIRegistry.getResourceString("WB_UNCONVERTED_GEOREFS_MSG"));
                    JPanel innerPane = new JPanel(new BorderLayout());
                    innerPane.add(lbl, BorderLayout.NORTH);
                    innerPane.add(unconvertedcells, BorderLayout.CENTER);
                    CustomDialog cd = new CustomDialog((Frame) UIRegistry.getTopWindow(),
                            UIRegistry.getResourceString("WB_UNCONVERTED_GEOREFS_TITLE"), false,
                            CustomDialog.OKHELP, innerPane);
                    cd.setHelpContext("UnconvertableGeoCoords");
                    UIHelper.centerAndShow(cd);
                }
            }
        };
        geoRefConvertDlg.setModal(false);
        toggle.setSelectedIndex(0);
        toggle.setOkBtn(geoRefConvertDlg.getOkBtn());
        toggle.createUI();
        geoRefConvertDlg.addWindowListener(new WindowListener() {

            /* (non-Javadoc)
             * @see java.awt.event.WindowStateListener#windowStateChanged(java.awt.event.WindowEvent)
             */
            @Override
            public void windowClosed(WindowEvent e) {
                geoRefConvertDlg = null;
            }

            /* (non-Javadoc)
             * @see java.awt.event.WindowListener#windowActivated(java.awt.event.WindowEvent)
             */
            @Override
            public void windowActivated(WindowEvent arg0) {
                // TODO Auto-generated method stub

            }

            /* (non-Javadoc)
             * @see java.awt.event.WindowListener#windowClosing(java.awt.event.WindowEvent)
             */
            @Override
            public void windowClosing(WindowEvent arg0) {
                // TODO Auto-generated method stub

            }

            /* (non-Javadoc)
             * @see java.awt.event.WindowListener#windowDeactivated(java.awt.event.WindowEvent)
             */
            @Override
            public void windowDeactivated(WindowEvent arg0) {
                // TODO Auto-generated method stub

            }

            /* (non-Javadoc)
             * @see java.awt.event.WindowListener#windowDeiconified(java.awt.event.WindowEvent)
             */
            @Override
            public void windowDeiconified(WindowEvent arg0) {
                // TODO Auto-generated method stub

            }

            /* (non-Javadoc)
             * @see java.awt.event.WindowListener#windowIconified(java.awt.event.WindowEvent)
             */
            @Override
            public void windowIconified(WindowEvent arg0) {
                // TODO Auto-generated method stub

            }

            /* (non-Javadoc)
             * @see java.awt.event.WindowListener#windowOpened(java.awt.event.WindowEvent)
             */
            @Override
            public void windowOpened(WindowEvent arg0) {
                // TODO Auto-generated method stub

            }

        });
        geoRefConvertDlg.setOkLabel(getResourceString("APPLY"));
        geoRefConvertDlg.setCancelLabel(getResourceString("CLOSE"));
        geoRefConvertDlg.setVisible(true);
    }

    /**
     * Show a map for any number of selected records.
     */
    protected void showMapOfSelectedRecords() {
        UsageTracker.incrUsageCount("WB.MapRows");

        log.debug("Showing map of selected records");
        //showMapBtn.setEnabled(false);
        int[] selection = spreadSheet.getSelectedRowModelIndexes();
        if (selection.length == 0) {
            // if none are selected, map all of them
            int rowCnt = spreadSheet.getRowCount();
            selection = new int[rowCnt];
            for (int i = 0; i < rowCnt; ++i) {
                selection[i] = spreadSheet.convertRowIndexToModel(i);
            }
        }

        DBTableIdMgr databaseSchema = WorkbenchTask.getDatabaseSchema();
        // build up a list of temporary MapLocationIFace records to feed to the LocalityMapper
        List<MapLocationIFace> mapLocations = new Vector<MapLocationIFace>(selection.length);
        List<WorkbenchRow> rows = workbench.getWorkbenchRowsAsList();
        int localityTableId = databaseSchema.getIdByClassName(Locality.class.getName());
        int lat1Index = workbench.getColumnIndex(localityTableId, "latitude1");
        int lon1Index = workbench.getColumnIndex(localityTableId, "longitude1");
        int lat2Index = workbench.getColumnIndex(localityTableId, "latitude2");
        int lon2Index = workbench.getColumnIndex(localityTableId, "longitude2");
        for (int i = 0; i < selection.length; ++i) {
            int index = selection[i];

            WorkbenchRow row = rows.get(index);

            String lat1 = row.getData(lat1Index);
            String lon1 = row.getData(lon1Index);
            Double latitude = null;
            Double longitude = null;
            try {
                GeoRefConverter converter = new GeoRefConverter();
                lat1 = converter.convert(StringUtils.stripToNull(lat1), GeoRefFormat.D_PLUS_MINUS.name());
                latitude = new Double(lat1);
                lon1 = converter.convert(StringUtils.stripToNull(lon1), GeoRefFormat.D_PLUS_MINUS.name());
                longitude = new Double(lon1);
            } catch (Exception e) {
                //UsageTracker.incrHandledUsageCount();
                //edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(WorkbenchPaneSS.class, e);
                // this could be a number format exception
                // or a null pointer exception if the field was empty
                // either way, we skip this record
                continue;
            }

            SimpleMapLocation newLoc = null;

            // try to use a bounding box
            if (lat2Index != -1 && lon2Index != -1) {
                String lat2 = row.getData((short) lat2Index);
                String lon2 = row.getData((short) lon2Index);
                Double latitude2 = null;
                Double longitude2 = null;

                try {
                    latitude2 = new Double(lat2);
                    longitude2 = new Double(lon2);
                } catch (Exception e) {
                    // this could be a number format exception
                    // or a null pointer exception if the field was empty
                    // either way, we'll just treat this record as though it only has lat1 and lon1
                }
                if ((latitude2 == null) ^ (longitude2 == null)) {
                    latitude2 = null;
                    longitude2 = null;
                }
                newLoc = new SimpleMapLocation(latitude, longitude, latitude2, longitude2);
            } else // use just the point
            {
                // we only have lat1 and long2
                newLoc = new SimpleMapLocation(latitude, longitude, null, null);
            }

            // add the storage to the list
            mapLocations.add(newLoc);
        }

        LocalityMapper mapper = new LocalityMapper(mapLocations);
        mapper.setMaxMapHeight(500);
        mapper.setMaxMapWidth(500);
        mapper.setShowArrows(false);
        mapper.setDotColor(new Color(64, 220, 64));
        MapperListener mapperListener = new MapperListener() {
            @SuppressWarnings("synthetic-access")
            public void exceptionOccurred(Exception e) {
                String errorMsg = null;
                if (e instanceof ConnectException) {
                    errorMsg = getResourceString("WB_MAP_SERVICE_CONNECTION_FAILURE");
                } else {
                    // in the future, we may want a different message for non-connection exceptions
                    //errorMsg = getResourceString("WB_MAP_SERVICE_CONNECTION_FAILURE");
                    errorMsg = e.getLocalizedMessage();
                }
                JStatusBar statusBar = UIRegistry.getStatusBar();
                statusBar.setErrorMessage(errorMsg, e);
                statusBar.setProgressDone(WorkbenchTask.WORKBENCH);
                showMapBtn.setEnabled(true);
                log.error("Exception while grabbing map from service", e);
            }

            public void mapReceived(Icon map) {
                mapImageReceived(map);
            }
        };

        //FileCache imageCache = UIRegistry.getLongTermFileCache();
        //imageCache.clear();
        mapper.getMap(mapperListener);

        JStatusBar statusBar = UIRegistry.getStatusBar();
        statusBar.setIndeterminate(WorkbenchTask.WORKBENCH, true);
        statusBar.setText(getResourceString("WB_CREATINGMAP"));
    }

    /**
     * Notification that the Map was received.
     * @param map icon of the map that was generated
     */
    protected void mapImageReceived(final Icon map) {
        JStatusBar statusBar = UIRegistry.getStatusBar();
        statusBar.setProgressDone(WorkbenchTask.WORKBENCH);
        statusBar.setText("");

        if (map != null) {
            UIHelper.positionFrameRelativeToTopFrame(mapFrame);
            mapFrame.setVisible(true);
            mapImageLabel.setIcon(map);
            //showMapBtn.setEnabled(true);

            // is the map really skinny?
            int ht = map.getIconHeight();
            int wd = map.getIconWidth();
            if (ht < 20 && wd > 100 || ht > 100 && wd < 20) {
                statusBar.setWarningMessage("WB_THIN_MAP_WARNING");
            }
        }
    }

    /**
     * @param columnIdx
     * @param rowIndex
     * @return
     */
    protected String getLatLonSrc(int columnIdx, int rowIndex) {
        WorkbenchTemplateMappingItem map = workbench.getMappingFromColumn((short) columnIdx);
        String result = null;
        if (map.getTableName().equals("locality")) {
            if (map.getFieldName().equalsIgnoreCase("latitude1")) {
                result = workbench.getRow(rowIndex).getLat1Text();
            } else if (map.getFieldName().equalsIgnoreCase("latitude2")) {
                result = workbench.getRow(rowIndex).getLat2Text();
            } else if (map.getFieldName().equalsIgnoreCase("longitude1")) {
                result = workbench.getRow(rowIndex).getLong1Text();
            } else if (map.getFieldName().equalsIgnoreCase("longitude2")) {
                result = workbench.getRow(rowIndex).getLong2Text();
            }
            return result;
        }

        return null;
    }

    protected List<CellPosition> convertColumnContents(int columnIndex, int[] rows, GeoRefConverter converter,
            String outputFormat) {
        return convertColumnContents(columnIndex, rows, converter, outputFormat,
                LatLonConverter.LATLON.Latitude /*dummy*/, LatLonConverter.DEGREES_FORMAT.None);
    }

    /**
     * Converts the column contents from on format of Lat/Lon to another
     * @param columnIndex the index of the column being converted
     * @param converter the converter to use
     * @param outputFormat the format string
     * 
     * return number of non-blank cells that were NOT converted
     */
    protected List<CellPosition> convertColumnContents(int columnIndex, int[] rows, GeoRefConverter converter,
            String outputFormat, LatLonConverter.LATLON latOrLon, LatLonConverter.DEGREES_FORMAT degFmt) {
        List<CellPosition> unconverted = new Vector<CellPosition>();

        if (columnIndex == -1) {
            return unconverted;
        }

        final int[] selectedRows = spreadSheet.getSelectedRows();
        final int[] selectedCols = spreadSheet.getSelectedColumns();
        for (int index = 0; index < rows.length; ++index) {
            int rowIndex = rows[index];
            String currentValue = null;
            //check backup col for original value before any conversions...
            currentValue = getLatLonSrc(columnIndex, rowIndex);
            if (currentValue != null) {
                currentValue = currentValue.replace("  ", " ");
            }
            if (StringUtils.isBlank(currentValue)) {
                currentValue = (String) model.getValueAt(rowIndex, columnIndex);
            }

            if (StringUtils.isBlank(currentValue)) {
                continue;
            }

            String convertedValue;
            try {
                convertedValue = converter.convert(StringUtils.stripToNull(currentValue), outputFormat, latOrLon,
                        degFmt);

            } catch (Exception e) {
                //UsageTracker.incrHandledUsageCount();
                //edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(WorkbenchPaneSS.class, e);
                // this value didn't convert correctly
                // it would be nice to highlight that cell, but I don't know how we could do that
                log.warn("Could not convert contents of cell (" + (rowIndex + 1) + "," + (columnIndex + 1) + ")");
                unconverted.add(new CellPosition(rowIndex, columnIndex));
                continue;
            }

            model.setValueAt(convertedValue, rowIndex, columnIndex, !(converter instanceof GeoRefConverter));
            if (!currentValue.equals(convertedValue)) {
                setChanged(true);
            }
        }

        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                ListSelectionModel selModel = spreadSheet.getSelectionModel();
                for (int rowIndex : selectedRows) {
                    selModel.addSelectionInterval(rowIndex, rowIndex);
                }
                ListSelectionModel colSelModel = spreadSheet.getColumnModel().getSelectionModel();
                for (int colIndex : selectedCols) {
                    colSelModel.addSelectionInterval(colIndex, colIndex);
                }
            }
        });
        return unconverted;
    }

    /**
     * Export to XLS .
     */
    protected void doExcelCsvExport() {
        int[] selection = spreadSheet.getSelectedRowModelIndexes();
        if (selection.length == 0) {
            // if none are selected, map all of them
            int rowCnt = spreadSheet.getRowCount();
            selection = new int[rowCnt];
            for (int i = 0; i < rowCnt; ++i) {
                selection[i] = spreadSheet.convertRowIndexToModel(i);
            }
        }

        // put all the selected rows in a List
        List<WorkbenchRow> selectedRows = new Vector<WorkbenchRow>();
        List<WorkbenchRow> rows = workbench.getWorkbenchRowsAsList();
        for (int i = 0; i < selection.length; ++i) {
            int index = selection[i];
            WorkbenchRow row = rows.get(index);
            selectedRows.add(row);
        }

        CommandAction command = new CommandAction(PluginsTask.PLUGINS, PluginsTask.EXPORT_LIST);
        command.setData(selectedRows);
        command.setProperty("tool", ExportToFile.class);

        Properties props = new Properties();

        if (!WorkbenchTask.getExportInfo(props, workbench.getName())) {
            return;
        }

        ConfigureExternalDataIFace config = ExportFileConfigurationFactory.getConfiguration(props);

        // Could get config to interactively get props or to look them up from prefs or ???
        // for now hard coding stuff...

        // add headers. all the time for now.
        config.setFirstRowHasHeaders(true);
        Vector<WorkbenchTemplateMappingItem> colHeads = new Vector<WorkbenchTemplateMappingItem>();
        colHeads.addAll(workbench.getWorkbenchTemplate().getWorkbenchTemplateMappingItems());
        Collections.sort(colHeads);
        String[] heads = new String[colHeads.size()];
        for (int h = 0; h < colHeads.size(); h++) {
            heads[h] = colHeads.get(h).getCaption();
        }
        config.setHeaders(heads);

        command.addProperties(config.getProperties());

        UsageTracker.incrUsageCount("WB.ExportXLSRowsTool");
        CommandDispatcher.dispatch(command);
    }

    /**
     * @return
     */
    protected Pair<WorkbenchTemplateMappingItem, List<WorkbenchTemplateMappingItem>> selectColumnName() {
        WorkbenchTemplateMappingItem genus = null;
        WorkbenchTemplateMappingItem species = null;
        WorkbenchTemplateMappingItem subspecies = null;
        WorkbenchTemplateMappingItem variety1 = null;

        Comparator<WorkbenchTemplateMappingItem> wbmtiComp = new Comparator<WorkbenchTemplateMappingItem>() {
            public int compare(WorkbenchTemplateMappingItem wbmti1, WorkbenchTemplateMappingItem wbmti2) {
                return wbmti1.getTitle().compareTo(wbmti2.getTitle());
            }

        };

        Vector<WorkbenchTemplateMappingItem> list = new Vector<WorkbenchTemplateMappingItem>();
        for (WorkbenchTemplateMappingItem item : workbench.getWorkbenchTemplate()
                .getWorkbenchTemplateMappingItems()) {
            //item.setUseCaptionForText(true);
            list.add(item);
            if (item.getFieldName().equals("genus1")) {
                genus = item;

            } else if (item.getFieldName().equals("species1")) {
                species = item;

            } else if (item.getFieldName().equals("subspecies1")) {
                subspecies = item;

            } else if (item.getFieldName().equals("variety1")) {
                variety1 = item;
            }
        }

        Collections.sort(list, wbmtiComp);

        if (genus != null && species != null) {
            WorkbenchTemplateMappingItem genusSpecies = new WorkbenchTemplateMappingItem();
            genusSpecies.setCaption(genus.getTitle() + " " + species.getTitle()
                    + (subspecies != null ? (" " + subspecies.getTitle()) : "")
                    + (variety1 != null ? (" " + variety1.getTitle()) : ""));
            genusSpecies.setViewOrder((short) -1);
            genusSpecies.setWorkbenchTemplateMappingItemId((int) genus.getViewOrder());
            genusSpecies.setVersion(species.getViewOrder());

            if (subspecies != null) {
                genusSpecies.setOrigImportColumnIndex(subspecies.getViewOrder());
            }
            if (variety1 != null) {
                genusSpecies.setSrcTableId((int) variety1.getViewOrder());
            }
            list.insertElementAt(genusSpecies, 0);
        }

        WorkbenchTemplateMappingItem rowItem = new WorkbenchTemplateMappingItem();
        rowItem.setCaption("Row Number"); // I18N
        rowItem.setViewOrder((short) -2);
        list.insertElementAt(rowItem, 0);

        final ToggleButtonChooserPanel<WorkbenchTemplateMappingItem> titlePanel = new ToggleButtonChooserPanel<WorkbenchTemplateMappingItem>(
                list, "GE_CHOOSE_FIELD_FOR_TITLE_EXPORT", ToggleButtonChooserPanel.Type.RadioButton);
        titlePanel.setUseScrollPane(true);
        titlePanel.createUI();

        Vector<WorkbenchTemplateMappingItem> includeList = new Vector<WorkbenchTemplateMappingItem>();
        for (WorkbenchTemplateMappingItem item : workbench.getWorkbenchTemplate()
                .getWorkbenchTemplateMappingItems()) {
            includeList.add(item);
        }

        Collections.sort(includeList, wbmtiComp);

        final ToggleButtonChooserPanel<WorkbenchTemplateMappingItem> inclPanel = new ToggleButtonChooserPanel<WorkbenchTemplateMappingItem>(
                includeList, "GE_CHOOSE_FIELDS_EXPORT", ToggleButtonChooserPanel.Type.Checkbox);
        inclPanel.setUseScrollPane(true);
        inclPanel.setAddSelectAll(true);
        inclPanel.createUI();

        PanelBuilder pb = new PanelBuilder(new FormLayout("f:p:g,10px,f:p:g", "f:p:g,6px"));
        CellConstraints cc = new CellConstraints();
        pb.add(titlePanel, cc.xy(1, 1));
        pb.add(inclPanel, cc.xy(3, 1));

        CustomDialog dlg = new CustomDialog((Frame) UIRegistry.getTopWindow(),
                getResourceString("GE_CHOOSE_FIELD_FOR_EXPORT_TITLE"), true, pb.getPanel()) {

            /* (non-Javadoc)
             * @see edu.ku.brc.ui.CustomDialog#okButtonPressed()
             */
            @Override
            protected void okButtonPressed() {
                if (titlePanel.getSelectedObject() != null && inclPanel.getSelectedObjects() != null
                        && inclPanel.getSelectedObjects().size() > 0) {
                    super.okButtonPressed();
                } else {
                    UIRegistry.showLocalizedError("WB_GOOGLE_SETTINGS_INCOMPLETE");
                }
            }

        };
        dlg.setVisible(true);
        //        for (WorkbenchTemplateMappingItem item : workbench.getWorkbenchTemplate().getWorkbenchTemplateMappingItems())
        //        {
        //            item.setUseCaptionForText(false);
        //        }

        if (!dlg.isCancelled()) {
            return new Pair<WorkbenchTemplateMappingItem, List<WorkbenchTemplateMappingItem>>(
                    titlePanel.getSelectedObject(), inclPanel.getSelectedObjects());
        }
        return null;
    }

    /**
     * @param viewOrder
     * @return
     */
    protected WorkbenchTemplateMappingItem getWBMI(final int viewOrder) {
        for (WorkbenchTemplateMappingItem item : workbench.getWorkbenchTemplate()
                .getWorkbenchTemplateMappingItems()) {
            if (item.getViewOrder().intValue() == viewOrder) {
                return item;
            }
        }
        return null;
    }

    /**
     * Make a request to the ExportTask to display the selected records in GoogleEarth.
     */
    protected void showRecordsInGoogleEarth() {
        UsageTracker.incrUsageCount("WB.GoogleEarthRows");

        log.info("Showing map of selected records");
        int[] selection = spreadSheet.getSelectedRowModelIndexes();
        if (selection.length == 0) {
            // if none are selected, map all of them
            int rowCnt = spreadSheet.getRowCount();
            selection = new int[rowCnt];
            for (int i = 0; i < rowCnt; ++i) {
                selection[i] = spreadSheet.convertRowIndexToModel(i);
            }
        }

        Pair<WorkbenchTemplateMappingItem, List<WorkbenchTemplateMappingItem>> configPair = selectColumnName();
        if (configPair == null || configPair.first == null || configPair.second == null
                || configPair.second.size() == 0) {
            return;
        }

        WorkbenchTemplateMappingItem genus = null;
        WorkbenchTemplateMappingItem species = null;
        WorkbenchTemplateMappingItem subspecies = null;
        WorkbenchTemplateMappingItem variety = null;

        WorkbenchTemplateMappingItem item = configPair.first;
        if (item.getViewOrder() == -1) {
            genus = getWBMI(item.getWorkbenchTemplateMappingItemId());
            species = getWBMI(item.getVersion());
            if (item.getOrigImportColumnIndex() != null) {
                subspecies = getWBMI(item.getOrigImportColumnIndex());
            }
            if (item.getSrcTableId() != null) {
                variety = getWBMI(item.getSrcTableId());
            }
        }

        // put all the selected rows in a List
        List<LatLonPlacemarkIFace> selectedRows = new Vector<LatLonPlacemarkIFace>();
        List<WorkbenchRow> rows = workbench.getWorkbenchRowsAsList();
        for (int i = 0; i < selection.length; ++i) {
            int index = selection[i];
            WorkbenchRow row = rows.get(index);

            short viewOrder = item.getViewOrder();
            String title = null;
            if (viewOrder == -1 && genus != null && species != null) {
                title = row.getData(genus.getViewOrder()) + " " + row.getData(species.getViewOrder())
                        + (subspecies != null ? (" " + row.getData(subspecies.getViewOrder())) : "")
                        + (variety != null ? (" " + row.getData(variety.getViewOrder())) : "");
            }

            if ((viewOrder == -1 && StringUtils.isEmpty(title)) || viewOrder == -2) {
                int visibleRowNumber = spreadSheet.convertRowIndexToView(index);
                title = "Row " + (visibleRowNumber + 1);

            } else if (viewOrder > -1) {
                title = row.getData(item.getViewOrder());
            }
            selectedRows.add(new WorkbenchRowPlacemarkWrapper(row, title, configPair.second));
        }

        // get an icon URL that is specific to the current context
        CommandAction command = new CommandAction(PluginsTask.PLUGINS, PluginsTask.EXPORT_LIST);
        command.setData(selectedRows);
        command.setProperty("tool", GoogleEarthExporter.class);
        command.setProperty("description", workbench.getRemarks() != null ? workbench.getRemarks() : "");

        JStatusBar statusBar = UIRegistry.getStatusBar();
        statusBar.setText(UIRegistry.getResourceString("WB_OPENING_GOOGLE_EARTH"));
        CommandDispatcher.dispatch(command);
    }

    /**
     * Checks to see if the template can support BG.
     * @return return whether this template supports BG
     */
    protected boolean isTemplateBGCompatible() {
        // look for the locality fields
        int localityTableId = Locality.getClassTableId();
        if (workbench.getColumnIndex(localityTableId, "localityName") == -1 || // I18N
                workbench.getColumnIndex(localityTableId, "latitude1") == -1
                || workbench.getColumnIndex(localityTableId, "longitude1") == -1) {
            return false;
        }

        // look for the geography fields
        int geographyTableId = Geography.getClassTableId();
        if (workbench.getColumnIndex(geographyTableId, "Country") == -1 || // I18N
                workbench.getColumnIndex(geographyTableId, "State") == -1
                || workbench.getColumnIndex(geographyTableId, "County") == -1) {
            return false;
        }
        return true;
    }

    /**
     * @param tableId
     * @param fieldName
     * @return
     */
    private String getTitleForField(final int tableId, final String fieldName) {
        String title = DBTableIdMgr.getInstance().getTitleForField(tableId, fieldName);
        return StringUtils.isNotEmpty(title) ? title : fieldName;
    }

    /**
     * @param missingCols
     * @param tblId
     * @param fieldNames
     */
    private void checkForGeoFields(final List<String> missingCols, final int tblId, final String... fieldNames) {
        for (String fldName : fieldNames) {
            if (workbench.getColumnIndex(tblId, fldName) == -1) {
                missingCols.add(getTitleForField(tblId, fldName));
            }
        }
    }

    /**
     * @return
     */
    /**
     * @param checkAllFields true checks all fields, false checks just Lat, Lon
     * @return
     */
    protected String[] getMissingButRequiredColumnsForGeoRefTool(final boolean checkAllFields, final String tool) {
        List<String> missingCols = new Vector<String>();

        if (checkAllFields) {
            checkForGeoFields(missingCols, Locality.getClassTableId(), "localityName", "latitude1", "longitude1");
            if ("geolocate".equalsIgnoreCase(tool)) {
                checkForGeoFields(missingCols, Geography.getClassTableId(), "country", "state");
            } else {
                checkForGeoFields(missingCols, Geography.getClassTableId(), "country", "state", "county");
            }
        } else {
            checkForGeoFields(missingCols, Locality.getClassTableId(), "latitude1", "longitude1");
        }

        // convert to a String[]  (toArray() converts to a Object[])
        String[] reqdFields = new String[missingCols.size()];
        for (int i = 0; i < missingCols.size(); ++i) {
            String s = missingCols.get(i);
            reqdFields[i] = s;
        }
        return reqdFields;
    }

    /**
     * @return
     */
    protected String[] getMissingButRequiredColumnsForGeoRefTool(final String tool) {
        return getMissingButRequiredColumnsForGeoRefTool(true, tool);
    }

    /**
     * @return
     */
    /**
     * @return
     */
    protected String[] getMissingGeoRefLatLonFields() {
        return getMissingButRequiredColumnsForGeoRefTool(false, "");
    }

    /**
     * @return the selected rows form the workbench
     */
    protected List<GeoCoordDataIFace> getSelectedRowsFromViewForGeoRef() {
        return new Vector<GeoCoordDataIFace>(getSelectedRows());
    }

    /**
     * @return
     */
    public List<WorkbenchRow> getSelectedRows() {
        // get the indexes into the model for all of the selected rows
        int[] selection = spreadSheet.getSelectedRowModelIndexes();
        if (selection.length == 0) {
            // if none are selected, map all of them
            int rowCnt = spreadSheet.getRowCount();
            selection = new int[rowCnt];
            for (int i = 0; i < rowCnt; ++i) {
                selection[i] = spreadSheet.convertRowIndexToModel(i);
            }
        }

        // gather all of the WorkbenchRows into a vector
        List<WorkbenchRow> rows = workbench.getWorkbenchRowsAsList();
        List<WorkbenchRow> selectedRows = new Vector<WorkbenchRow>();
        for (int i : selection) {
            selectedRows.add(rows.get(i));
        }
        return selectedRows;
    }

    /**
     * @param geoRefService
     * @param trackId
     */
    protected void doGeoRef(final GeoCoordServiceProviderIFace geoRefService, final String trackId) {
        UsageTracker.incrUsageCount(trackId);
        log.info("Performing GeoREflookup of selected records: " + trackId);

        if (true) {
            List<GeoCoordDataIFace> selectedWBRows = getSelectedRowsFromViewForGeoRef();
            if (selectedWBRows != null) {
                geoRefService.processGeoRefData(selectedWBRows, new GeoCoordProviderListenerIFace() {
                    public void aboutToDisplayResults() {
                        if (imageFrame != null) {
                            imageFrame.setAlwaysOnTop(false);
                        }
                    }

                    public void complete(final List<GeoCoordDataIFace> items, final int itemsUpdated) {
                        if (itemsUpdated > 0) {
                            setChanged(true);
                            int[] selection = spreadSheet.getSelectedRowModelIndexes();
                            if (selection.length > 0) {
                                validateRows(selection);
                            } else {
                                validateAll(null);
                            }
                            model.fireDataChanged();
                            spreadSheet.repaint();
                        }
                    }
                }, "WorkbenchSpecialTools"); // last argument is Help Context
            }
            return;
        }
    }

    /**
     * Set that there has been a change.
     * 
     * @param changed true or false
     */
    public void setChanged(final boolean changed) {
        if (!blockChanges) {
            hasChanged = changed;
            saveBtn.setEnabled(hasChanged);
            updateUploadBtnState();
        }
    }

    /**
     * @return whether there has been a change.
     */
    public boolean isChanged() {
        return hasChanged;
    }

    /**
     * @param wbtmi
     * @return
     */
    public static int getMaxColWidth(WorkbenchTemplateMappingItem wbtmi) {
        DBFieldInfo fi = null;
        int result = WorkbenchDataItem.getMaxWBCellLength();
        if (wbtmi.getFieldInfo() != null) {
            fi = wbtmi.getFieldInfo();
        } else {
            DBTableIdMgr databaseSchema = WorkbenchTask.getDatabaseSchema();
            DBTableInfo ti = databaseSchema.getInfoById(wbtmi.getSrcTableId());
            if (ti != null) {
                fi = ti.getFieldByName(wbtmi.getFieldName());
                if (fi != null) {
                    wbtmi.setFieldInfo(fi);
                } else {
                    log.error("Can't find field with name [" + wbtmi.getFieldName() + "]");
                }
            } else {
                log.error("Can't find table [" + wbtmi.getSrcTableId() + "]");
            }
        }
        if (fi != null && RecordTypeCodeBuilder.getTypeCode(fi) == null && fi.getLength() > 0) {
            result = Math.min(fi.getLength(), WorkbenchDataItem.getMaxWBCellLength());
        }
        return result;
    }

    /**
     * @param workbench
     * @return
     */
    public static Integer[] getMaxColWidths(Workbench workbench) {
        List<WorkbenchTemplateMappingItem> wbtmis = new ArrayList<WorkbenchTemplateMappingItem>();
        wbtmis.addAll(workbench.getWorkbenchTemplate().getWorkbenchTemplateMappingItems());
        Collections.sort(wbtmis);
        Integer[] result = new Integer[wbtmis.size()];
        for (int i = 0; i < wbtmis.size(); i++) {
            result[i] = new Integer(WorkbenchPaneSS.getMaxColWidth(wbtmis.get(i)));
        }
        return result;
    }

    /**
     * Adjust all the column width for the data in the column, this may be handles with JDK 1.6 (6.)
     * @param tableArg the table that should have it's columns adjusted
     */
    private void initColumnSizes(final JTable tableArg, final JButton theSaveBtn) throws Exception {
        TableModel tblModel = tableArg.getModel();
        TableColumn column = null;
        Component comp = null;
        int headerWidth = 0;
        int cellWidth = 0;

        Element uploadDefs = null;
        if (WorkbenchTask.isCustomizedSchema()) {
            uploadDefs = XMLHelper.readFileToDOM4J(
                    new File(UIRegistry.getAppDataDir() + File.separator + "specify_workbench_upload_def.xml"));
        } else {
            uploadDefs = XMLHelper.readDOMFromConfigDir("specify_workbench_upload_def.xml");
        }

        //UIRegistry.getInstance().hookUpUndoableEditListener(cellEditor);

        Vector<WorkbenchTemplateMappingItem> wbtmis = new Vector<WorkbenchTemplateMappingItem>();
        wbtmis.addAll(workbench.getWorkbenchTemplate().getWorkbenchTemplateMappingItems());
        Collections.sort(wbtmis);

        DBTableIdMgr databaseSchema = WorkbenchTask.getDatabaseSchema();

        columnMaxWidths = new Integer[tableArg.getColumnCount()];
        for (int i = 0; i < wbtmis.size() /*tableArg.getColumnCount()*/; i++) {
            TableCellRenderer headerRenderer = tableArg.getColumnModel().getColumn(i).getHeaderRenderer();
            WorkbenchTemplateMappingItem wbtmi = wbtmis.elementAt(i);

            // Now go retrieve the data length
            int fieldWidth = WorkbenchDataItem.getMaxWBCellLength();
            DBTableInfo ti = databaseSchema.getInfoById(wbtmi.getSrcTableId());
            if (ti != null) {
                DBFieldInfo fi = ti.getFieldByName(wbtmi.getFieldName());
                if (fi != null) {
                    wbtmi.setFieldInfo(fi);
                    //System.out.println(fi.getName()+"  "+fi.getLength()+"  "+fi.getType());
                    if (RecordTypeCodeBuilder.getTypeCode(fi) == null && fi.getLength() > 0) {
                        fieldWidth = Math.min(fi.getLength(), WorkbenchDataItem.getMaxWBCellLength());
                    }
                } else {
                    log.error("Can't find field with name [" + wbtmi.getFieldName() + "]");
                }
            } else {
                log.error("Can't find table [" + wbtmi.getSrcTableId() + "]");
            }
            columnMaxWidths[i] = new Integer(fieldWidth);
            GridCellEditor cellEditor = getCellEditor(wbtmi, fieldWidth, theSaveBtn, uploadDefs);
            column = tableArg.getColumnModel().getColumn(i);

            comp = headerRenderer.getTableCellRendererComponent(null, column.getHeaderValue(), false, false, 0, 0);
            headerWidth = comp.getPreferredSize().width;

            comp = tableArg.getDefaultRenderer(tblModel.getColumnClass(i)).getTableCellRendererComponent(tableArg,
                    tblModel.getValueAt(0, i), false, false, 0, i);

            cellWidth = comp.getPreferredSize().width;

            //comp.setBackground(Color.WHITE);

            int maxWidth = headerWidth + 10;
            TableModel m = tableArg.getModel();
            FontMetrics fm = comp.getFontMetrics(comp.getFont());
            for (int row = 0; row < tableArg.getModel().getRowCount(); row++) {
                String text = m.getValueAt(row, i).toString();
                maxWidth = Math.max(maxWidth, fm.stringWidth(text) + 10);
                //log.debug(i+" "+maxWidth);
            }

            //XXX: Before Swing 1.1 Beta 2, use setMinWidth instead.
            //log.debug(Math.max(maxWidth, cellWidth));
            //log.debug(Math.min(Math.max(maxWidth, cellWidth), 400));
            column.setPreferredWidth(Math.min(Math.max(maxWidth, cellWidth), 400));

            column.setCellEditor(cellEditor);
        }
        //tableArg.setCellEditor(cellEditor);
    }

    /**
     * @param wbtmi
     * @return
     */
    protected GridCellEditor getCellEditor(WorkbenchTemplateMappingItem wbtmi, int fieldWidth, JButton theSaveBtn,
            Element uploadDefs) {
        PickListDBAdapterIFace pickList = null;
        DBTableInfo tblInfo = DBTableIdMgr.getInstance().getInfoByTableName(wbtmi.getTableName());
        if (tblInfo != null) {
            String fldName = wbtmi.getFieldName();
            @SuppressWarnings("unchecked")
            List<Object> flds = uploadDefs.selectNodes("field");
            for (Object fld : flds) {
                String table = XMLHelper.getAttr((Element) fld, "table", null);
                String field = XMLHelper.getAttr((Element) fld, "name", null);
                if (wbtmi.getTableName().equalsIgnoreCase(table) && wbtmi.getFieldName().equalsIgnoreCase(field)) {
                    fldName = XMLHelper.getAttr((Element) fld, "actualname", fldName);
                    break;
                }
            }
            DBFieldInfo fldInfo = tblInfo.getFieldByName(fldName);
            if (fldInfo != null) {
                if (!StringUtils.isEmpty(fldInfo.getPickListName())) {
                    pickList = PickListDBAdapterFactory.getInstance().create(fldInfo.getPickListName(), false);
                } else if (RecordTypeCodeBuilder.isTypeCodeField(fldInfo)) {
                    pickList = RecordTypeCodeBuilder.getTypeCode(fldInfo);
                }
            }
            if (tblInfo.getTableId() == Preparation.getClassTableId()) {
                fldName = wbtmi.getFieldName();
                if (fldName.startsWith("prepType") && StringUtils.isNumeric(fldName.replace("prepType", ""))) {
                    pickList = PickListDBAdapterFactory.getInstance().create("prepType", false);
                }

            }
        }
        if (pickList == null) {
            return new GridCellEditor(new JTextField(), wbtmi.getCaption(), fieldWidth, theSaveBtn);
        }
        JComboBox<PickListItemIFace> comboBox = new JComboBox<PickListItemIFace>(pickList.getList());
        comboBox.setEditable(true);
        return new GridCellListEditor(comboBox, wbtmi.getCaption(), fieldWidth, theSaveBtn);
    }

    /**
     * Carry forward configuration.
     */
    public void configCarryFoward() {
        Vector<WorkbenchTemplateMappingItem> items = new Vector<WorkbenchTemplateMappingItem>();
        Vector<WorkbenchTemplateMappingItem> selectedObjects = new Vector<WorkbenchTemplateMappingItem>();
        items.addAll(workbench.getWorkbenchTemplate().getWorkbenchTemplateMappingItems());

        for (WorkbenchTemplateMappingItem item : items) {
            if (item.getCarryForward()) {
                selectedObjects.add(item);
            }
        }

        // turn off alwaysOnTop for Swing repaint reasons (prevents a lock up)
        imageFrame.setAlwaysOnTop(false);

        Collections.sort(items);
        ToggleButtonChooserDlg<WorkbenchTemplateMappingItem> dlg = new ToggleButtonChooserDlg<WorkbenchTemplateMappingItem>(
                (Frame) UIRegistry.get(UIRegistry.FRAME), "WB_CARRYFORWARD", "WB_CHOOSE_CARRYFORWARD", items,
                CustomDialog.OKCANCELHELP, ToggleButtonChooserPanel.Type.Checkbox);

        dlg.setHelpContext(
                currentPanelType == PanelType.Spreadsheet ? "WorkbenchGridEditingCF" : "WorkbenchFormEditingCF");
        dlg.setAddSelectAll(true);
        dlg.setSelectedObjects(selectedObjects);
        dlg.setModal(true);
        dlg.setAlwaysOnTop(true);
        dlg.setUseScrollPane(true);
        dlg.setVisible(true);

        if (!dlg.isCancelled()) {
            for (WorkbenchTemplateMappingItem item : items) {
                item.setCarryForward(false);
            }
            for (WorkbenchTemplateMappingItem item : dlg.getSelectedObjects()) {
                item.setCarryForward(true);
            }
            setChanged(true);
        }
    }

    /**
     * loads workbench from the database and backs it up (exports to an xls file).
     */
    protected void backupObject() {
        WorkbenchBackupMgr.backupWorkbench(workbench);
    }

    protected void logDebug(Object toLog) {
        if (debugging) {
            log.debug(toLog);
        }
    }

    /**
     * Save the Data. 
     */
    public void saveObject() {
        if (workbench == null) {
            UIRegistry.showError("The workbench is 'null' before save.\nPlease contact Specify support.");
            return;
        }
        //backup current database contents for workbench
        logDebug("backupObject(): " + System.nanoTime());
        backupObject();
        logDebug("---------" + System.nanoTime());

        logDebug("checkCurrentEditState: " + System.nanoTime());
        checkCurrentEditState();
        logDebug("---------" + System.nanoTime());

        DataProviderSessionIFace session = DataProviderFactory.getInstance().createSession();

        boolean committed = false;
        boolean opened = false;
        try {
            FormHelper.updateLastEdittedInfo(workbench);

            logDebug("merging and saving: " + System.nanoTime());
            session.beginTransaction();
            opened = true;

            session.saveOrUpdate(workbench);
            session.commit();
            committed = true;
            session.flush();

            model.setWorkbench(workbench);
            formPane.setWorkbench(workbench);
            if (imageFrame != null) {
                imageFrame.setWorkbench(workbench);
            }

            log.info("Session Saved[ and Flushed " + session.hashCode() + "]");
            logDebug("-------- " + System.nanoTime());

            hasChanged = false;
            String msg = String.format(getResourceString("WB_SAVED"), new Object[] { workbench.getName() });
            UIRegistry.getStatusBar().setText(msg);
        } catch (StaleObjectException ex) // was StaleObjectStateException
        {
            UsageTracker.incrHandledUsageCount();
            edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(WorkbenchPaneSS.class, ex);
            if (opened && !committed) {
                session.rollback();
            }
            //recoverFromStaleObject("UPDATE_DATA_STALE");
            UnhandledExceptionDialog dlg = new UnhandledExceptionDialog(ex);
            dlg.setVisible(true);

        } catch (Exception ex) {
            edu.ku.brc.af.core.UsageTracker.incrHandledUsageCount();
            edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(WorkbenchPaneSS.class, ex);
            log.error("******* " + ex);
            ex.printStackTrace();
            if (opened && !committed) {
                session.rollback();
            }
            UnhandledExceptionDialog dlg = new UnhandledExceptionDialog(ex);
            dlg.setVisible(true);
        } finally {
            session.close();
            session = null;
        }
        if (saveBtn != null) {
            saveBtn.setEnabled(false);
        }
        if (datasetUploader != null) {
            datasetUploader.refresh();
        }
        updateUploadBtnState();
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.af.tasks.subpane.BaseSubPane#aboutToShutdown()
     */
    @Override
    public boolean aboutToShutdown() {
        super.aboutToShutdown();

        for (WorkBenchPluginIFace wbp : workBenchPlugins.values()) {
            wbp.shutdown();
        }

        // Tell it is about to be hidden.
        // this way it can end any editing
        if (formPane != null) {
            if (!checkCurrentEditState()) {
                SubPaneMgr.getInstance().showPane(this);
                // Need to reverify to get the error to display again.
                if (spreadSheet.getCellEditor() != null) {
                    spreadSheet.getCellEditor().stopCellEditing();
                }
                return false;
            }
        }

        if (datasetUploader != null && !datasetUploader.aboutToShutdown(this)) {
            return false;
        }

        if (datasetUploader != null) {
            if (datasetUploader.closing(this)) {
                datasetUploader = null;
                Uploader.unlockApp();
                Uploader.unlockUpload();
            }
        }

        boolean retStatus = true;
        if (hasChanged) {

            // turn off alwaysOnTop for Swing repaint reasons (prevents a lock up)
            if (imageFrame != null) {
                imageFrame.setAlwaysOnTop(false);
            }
            String msg = String.format(getResourceString("SaveChanges"), getTitle());
            JFrame topFrame = (JFrame) UIRegistry.getTopWindow();

            final String wbName = workbench.getName();

            int rv = JOptionPane.showConfirmDialog(topFrame, msg, getResourceString("SaveChangesTitle"),
                    JOptionPane.YES_NO_CANCEL_OPTION);
            if (rv == JOptionPane.YES_OPTION) {
                //GlassPane and Progress bar currently don't show up during shutdown
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        UIRegistry.writeSimpleGlassPaneMsg(
                                String.format(getResourceString("WB_SAVING"), new Object[] { workbench.getName() }),
                                WorkbenchTask.GLASSPANE_FONT_SIZE);
                        UIRegistry.getStatusBar().setIndeterminate(wbName, true);
                    }
                });

                SwingWorker saver = new SwingWorker() {

                    /* (non-Javadoc)
                     * @see edu.ku.brc.helpers.SwingWorker#construct()
                     */
                    @Override
                    public Object construct() {

                        Boolean result = null;
                        try {
                            saveObject();
                            result = new Boolean(true);
                        } catch (Exception ex) {
                            edu.ku.brc.af.core.UsageTracker.incrHandledUsageCount();
                            edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(WorkbenchPaneSS.class, ex);
                            log.error(ex);
                        }
                        return result;
                    }

                    /* (non-Javadoc)
                     * @see edu.ku.brc.helpers.SwingWorker#finished()
                     */
                    @Override
                    public void finished() {
                        UIRegistry.clearSimpleGlassPaneMsg();
                        UIRegistry.getStatusBar().setProgressDone(wbName);
                        shutdownLock.decrementAndGet();
                        shutdown();
                    }

                };
                shutdownLock.incrementAndGet();
                saver.start();
                //retStatus = saver.get() != null;
            } else if (rv == JOptionPane.CANCEL_OPTION || rv == JOptionPane.CLOSED_OPTION) {
                return false;
            } else if (rv == JOptionPane.NO_OPTION) {
                hasChanged = false; // we do this so we don't get asked a second time
            }

        }

        if (retStatus) {
            ((WorkbenchTask) ContextMgr.getTaskByClass(WorkbenchTask.class)).closing(this);
            ((SGRTask) ContextMgr.getTaskByClass(SGRTask.class)).closing(this);

            if (spreadSheet != null) {
                spreadSheet.getSelectionModel().removeListSelectionListener(workbenchRowChangeListener);
            }
            workbenchRowChangeListener = null;
        }

        return retStatus;
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.af.tasks.subpane.BaseSubPane#shutdown()
     */
    @Override
    public void shutdown() {
        //Check to see if background tasks accessing the Workbench are active.
        if (shutdownLock.get() > 0) {
            return;
        }

        if (spreadSheet == null) {
            return;
        }

        //--------------------------------------------------------------------------------
        // I really don't know how much of all this is necessary
        // but using the JProfiler it seems things got better when I did certain things.
        //--------------------------------------------------------------------------------

        UIRegistry.getLaunchFindReplaceAction().setSearchReplacePanel(null);
        UIRegistry.enableFind(null, false);

        JFrame topFrame = (JFrame) UIRegistry.getTopWindow();

        if (minMaxWindowListener != null) {
            topFrame.removeWindowListener(minMaxWindowListener);
            minMaxWindowListener = null;
        }

        shutdownValidators();

        removeAll();
        if (mainPanel != null) {
            mainPanel.removeAll();
        }
        if (controllerPane != null) {
            controllerPane.removeAll();
        }

        if (spreadSheet != null) {
            spreadSheet.setVisible(false);
            spreadSheet.getSelectionModel().removeListSelectionListener(workbenchRowChangeListener);
            spreadSheet.cleanUp();
            workbenchRowChangeListener = null;

            for (int i = 0; i < spreadSheet.getColumnCount(); i++) {
                TableColumn column = spreadSheet.getColumnModel().getColumn(i);
                TableCellEditor editor = column.getCellEditor();
                if (editor instanceof GridCellEditor) {
                    ((GridCellEditor) editor).cleanUp();
                }
            }
            TableCellEditor editor = spreadSheet.getCellEditor();
            if (editor instanceof GridCellEditor) {
                ((GridCellEditor) editor).cleanUp();
            }
        }

        if (imageFrame != null) {
            imageFrame.cleanUp();
            imageFrame.dispose();
        }

        if (mapFrame != null) {
            mapFrame.dispose();
        }

        if (model != null) {
            model.cleanUp();
        }

        if (headers != null) {
            headers.clear();
        }

        if (resultsetController != null) {
            resultsetController.removeListener(formPane);
            resultsetController = null;
        }

        if (formPane != null) {
            formPane.cleanup();
        }

        formPane = null;
        findPanel = null;
        spreadSheet = null;
        workbench = null;
        model = null;
        imageColExt = null;
        columns = null;
        imageFrame = null;
        headers = null;
        recordSet = null;
        mainPanel = null;
        controllerPane = null;
        currentPanelType = null;
        cardLayout = null;
        cpCardLayout = null;
        uploadToolPanel = null;
        workBenchPlugins = null;

        super.shutdown();
    }

    /**
     * 
     */
    protected void shutdownValidators() {
        //validationExecutor.shutdownNow();
        if (validationWorkerQueue.peek() != null) {
            //System.out.println("Shutdown: Cancelling validation worker.");
            ValidationWorker vw = null;
            synchronized (validationWorkerQueue) {
                vw = validationWorkerQueue.peek();
                validationWorkerQueue.clear();
            }
            if (vw != null && !vw.isDone()) {
                vw.cancel(true);
            }
        }

    }

    /* (non-Javadoc)
     * @see edu.ku.brc.af.tasks.subpane.BaseSubPane#showingPane(boolean)
     */
    @Override
    public void showingPane(boolean show) {
        //System.out.println("WBPane.showingPane(" + show + ") " + isVisible());
        if (formPane != null) {
            formPane.showingPane(show);
        }

        if (show) {
            UIRegistry.getLaunchFindReplaceAction().setSearchReplacePanel(findPanel);

            if (imageFrameWasShowing) {
                toggleImageFrameVisible();

                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        final Frame f = (Frame) UIRegistry.get(UIRegistry.FRAME);
                        f.toFront();
                        f.requestFocus();
                    }
                });
            }
        } else if (isVisible() == show /*fix for #9182*/) {
            checkCurrentEditState();

            if (imageFrame != null && imageFrame.isVisible()) {
                imageFrameWasShowing = true;
                toggleImageFrameVisible();
            } else {
                imageFrameWasShowing = false;
            }

            if (mapFrame != null && mapFrame.isVisible()) {
                mapFrame.setVisible(false);

            }
        }
        super.showingPane(show);
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.af.tasks.subpane.BaseSubPane#getRecordSet()
     */
    @Override
    public RecordSetIFace getRecordSet() {
        if (recordSet == null) {
            recordSet = new RecordSet();
            recordSet.initialize();
            recordSet.set(workbench.getName(), Workbench.getClassTableId(), RecordSet.GLOBAL);
            recordSet.addItem(workbench.getWorkbenchId());
        }
        return recordSet;
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.af.tasks.subpane.BaseSubPane#getHelpTarget()
     */
    @Override
    public String getHelpTarget() {
        return currentPanelType == PanelType.Spreadsheet ? "WorkbenchGridEditing" : "WorkbenchFormEditing";
    }

    /**
     * @return  a list of open panes that prohibit uploading.
     */
    protected List<SubPaneIFace> checkOpenTasksForUpload() {
        List<SubPaneIFace> result = new LinkedList<SubPaneIFace>();
        for (SubPaneIFace pane : SubPaneMgr.getInstance().getSubPanes()) {
            if (prohibitsUpload(pane)) {
                result.add(pane);
            }
        }
        return result;
    }

    /**
     * @return  a list of open panes that need to be closed when uploader is closed.
     */
    protected List<SubPaneIFace> checkOpenTasksForUploadClose() {
        List<SubPaneIFace> result = new LinkedList<SubPaneIFace>();
        for (SubPaneIFace pane : SubPaneMgr.getInstance().getSubPanes()) {
            if (pane instanceof ESResultsSubPane) {
                result.add(pane);
            }
        }
        return result;
    }

    /**
     * @param pane
     * @return true if uploads are prohibited while pane is open.
     */
    protected boolean prohibitsUpload(final SubPaneIFace pane) {
        if (pane.getTask().getClass().equals(DataEntryTask.class)) {
            return true;
        }
        if (pane.getTask().getClass().equals(InteractionsTask.class)) {
            return true;
        }
        if (pane instanceof ESResultsSubPane) {
            return true;
        }
        return false;
    }

    /**
     * @param badPanes
     * @return a list of the distinct tasks represented by badPanes 
     */
    protected String getListOfBadTasks(final List<SubPaneIFace> badPanes) {
        Set<Taskable> badTasks = new HashSet<Taskable>();
        for (SubPaneIFace pane : badPanes) {
            badTasks.add(pane.getTask());
        }
        String result = "";
        Iterator<Taskable> badI = badTasks.iterator();
        while (badI.hasNext()) {
            Taskable badTask = badI.next();
            if (!StringUtils.isBlank(result)) {
                if (badI.hasNext()) {
                    result += ", ";
                } else {
                    result += " or ";
                }
            }
            if (badTask instanceof ExpressSearchTask) {
                result += UIRegistry.getResourceString("WorkbenchPaneSS.SearchResult");
            } else {
                result += badTask.getTitle();
            }
        }
        return result;
    }

    /**
     * builds validator
     */
    protected void buildValidator() {
        buildValidator(false);
    }

    /**
     * builds validator
     */
    protected void buildValidator(boolean quietly) {
        try {
            workbenchValidator = new WorkbenchValidator(this);
            setMatchStatusForUploadTables();
            //set up catnum checker
            //           UploadTable cout = workbenchValidator.getUploader().getUploadTableByName("collectionobject");
            //           if (cout != null)
            //           {
            //              for (Vector<UploadField> ufs : cout.getUploadFields())
            //              {
            //                 for (UploadField uf : ufs)
            //                 {
            //                    DBFieldInfo fi = uf.getField() != null ? uf.getField().getFieldInfo() : null;
            //                    if (fi != null && fi.getColumn().equalsIgnoreCase("CatalogNumber"))
            //                    {
            //                       catNumCol = uf.getIndex();
            //                       break;
            //                    }
            //                 }
            //              }
            //              if (catNumCol != -1)
            //              {
            //                 catNumChecker = new UniquenessChecker();
            //              }
            //           }
        } catch (Exception ex) {
            if (ex instanceof WorkbenchValidator.WorkbenchValidatorException || ex instanceof UploaderException) {
                WorkbenchValidator.WorkbenchValidatorException wvEx = null;
                if (ex instanceof WorkbenchValidator.WorkbenchValidatorException) {
                    wvEx = (WorkbenchValidator.WorkbenchValidatorException) ex;
                } else if (ex.getCause() instanceof WorkbenchValidator.WorkbenchValidatorException) {
                    wvEx = (WorkbenchValidator.WorkbenchValidatorException) ex.getCause();
                }
                if (!quietly && wvEx != null && wvEx.getStructureErrors().size() > 0) {
                    Uploader.showStructureErrors(wvEx.getStructureErrors());
                }
            } else {
                ex.printStackTrace();
            }
            if (!quietly) {
                UIRegistry.showLocalizedError("WorkbenchPaneSS.UnableToAutoValidate");
            }
            if (isDoIncrementalValidation() || isDoIncrementalMatching()) //but how could this be true???
            //oh yeah. If they were on when the workbench was last closed, but the template has been changed since...
            {
                uploadToolPanel.turnOffSelections();
                if (isDoIncrementalValidation()) {
                    turnOffIncrementalValidation();
                }
                if (isDoIncrementalMatching()) {
                    turnOffIncrementalMatching();
                }
                model.fireDataChanged();
            }
            workbenchValidator = null;
        }
    }

    /**
     * 
     */
    public void turnOffIncrementalValidation() {
        boolean savedBlockChanges = blockChanges;
        try {
            blockChanges = true;
            shutdownValidators();
            doIncrementalValidation = false;
            updateBtnUI();
            //workbenchValidator = null;
            model.fireDataChanged();
            AppPreferences.getLocalPrefs().putBoolean(wbAutoValidatePrefName + "." + workbench.getId(),
                    doIncrementalValidation);
        } finally {
            blockChanges = savedBlockChanges;
        }
    }

    /**
     * 
     */
    public void turnOffIncrementalMatching() {
        boolean savedBlockChanges = blockChanges;
        try {
            blockChanges = true;
            shutdownValidators();
            doIncrementalMatching = false;
            //workbenchValidator = null;
            model.fireDataChanged();
            AppPreferences.getLocalPrefs().putBoolean(wbAutoMatchPrefName + "." + workbench.getId(),
                    doIncrementalMatching);
        } finally {
            blockChanges = savedBlockChanges;
        }
    }

    public void doDatasetUpload() {
        if (datasetUploader != null) {
            //the button shouldn't be enabled in this case, but just to be sure:
            log.error("The upload button was enabled but the datasetUploader was not null.");
            return;
        }

        List<SubPaneIFace> badPanes = checkOpenTasksForUpload();
        if (badPanes.size() > 0) {
            UIRegistry.displayInfoMsgDlgLocalized(
                    String.format(getResourceString("WB_UPLOAD_CLOSE_ALL_MSG"), getListOfBadTasks(badPanes)));
            return;
        }

        List<String> logins = ((SpecifyAppContextMgr) AppContextMgr.getInstance())
                .getAgentListLoggedIn(AppContextMgr.getInstance().getClassObject(Discipline.class));
        if (logins.size() > 0) {
            String loginStr = "";
            for (int l = 0; l < logins.size(); l++) {
                if (l > 0) {
                    loginStr += ", ";
                }
                loginStr += "'" + logins.get(l) + "'";
            }
            PanelBuilder pb = new PanelBuilder(
                    new FormLayout("5dlu, f:p:g, 5dlu", "5dlu, f:p:g, 2dlu, f:p:g, 2dlu, f:p:g, 5dlu"));
            pb.add(new JLabel(UIRegistry.getResourceString("WB_UPLOAD_OTHER_USERS")),
                    new CellConstraints().xy(2, 2));
            pb.add(new JLabel(loginStr), new CellConstraints().xy(2, 4));
            pb.add(new JLabel(UIRegistry.getResourceString("WB_UPLOAD_OTHER_USERS2")),
                    new CellConstraints().xy(2, 6));

            CustomDialog dlg = new CustomDialog((Frame) UIRegistry.getTopWindow(),
                    UIRegistry.getResourceString("WB_UPLOAD_DENIED_DLG"), true, CustomDialog.OKCANCELAPPLYHELP,
                    pb.getPanel());
            dlg.setApplyLabel(UIRegistry.getResourceString("WB_UPLOAD_OVERRIDE"));
            dlg.setCloseOnApplyClk(true);
            dlg.createUI();

            //Stoopid x-box...
            dlg.getOkBtn().setVisible(false);
            dlg.setCancelLabel(dlg.getOkBtn().getText());
            //...Stoopid x-box

            UIHelper.centerAndShow(dlg);
            dlg.dispose();
            if (dlg.isCancelled()) {
                return;
            }
            PanelBuilder pb2 = new PanelBuilder(new FormLayout("5dlu, f:p:g, 5dlu", "5dlu, f:p:g, 5dlu"));
            pb2.add(new JLabel(UIRegistry.getResourceString("WB_UPLOAD_CONFIRM_ANNIHILATION")),
                    new CellConstraints().xy(2, 2));
            CustomDialog dlg2 = new CustomDialog((Frame) UIRegistry.getTopWindow(),
                    UIRegistry.getResourceString("WB_UPLOAD_DANGER"), true, CustomDialog.OKCANCELHELP,
                    pb2.getPanel());
            UIHelper.centerAndShow(dlg2);
            dlg2.dispose();
            if (dlg2.isCancelled()) {
                return;
            }
        }

        WorkbenchUploadMapper importMapper = new WorkbenchUploadMapper(workbench.getWorkbenchTemplate());
        try {
            Vector<UploadMappingDef> maps = importMapper.getImporterMapping();
            DB db = new DB();
            if (Uploader.lockUpload(null, true) != Uploader.LOCKED) {
                return;
            }
            Uploader.lockApp();
            spreadSheet.clearSorter();
            datasetUploader = new Uploader(db, new UploadData(maps, workbench.getWorkbenchRowsAsList()), this,
                    false);
            Vector<UploadMessage> structureErrors = datasetUploader.verifyUploadability();
            if (structureErrors.size() > 0) {
                Uploader.showStructureErrors(structureErrors);
                uploadDone();
                return;
            }
            if (!datasetUploader.setAdditionalLocks()) {
                uploadDone();
                return;
            }

            UploadField[] configs = uploadToolPanel.getConfiguredFields();
            datasetUploader.copyFldConfigs(configs);

            uploadPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, spreadSheet.getScrollPane(),
                    datasetUploader.getMainPanel());
            mainPanel.remove(spreadSheet.getScrollPane());
            uploadPane.setOneTouchExpandable(true);
            uploadPane.setDividerLocation(spreadSheet.getScrollPane().getHeight() * 2 / 3);
            spreadSheet.getScrollPane().setVisible(true);

            // Provide minimum sizes for the two components in the split pane
            Dimension minimumSize = new Dimension(200, 200);
            datasetUploader.getMainPanel().setMinimumSize(minimumSize);
            spreadSheet.getScrollPane().setMinimumSize(minimumSize);
            mainPanel.add(uploadPane, PanelType.Spreadsheet.toString());
            showPanel(PanelType.Spreadsheet);
            mainPanel.validate();
            mainPanel.doLayout();
            ssFormSwitcher.setEnabled(false);
            // next line causes some weird behavior: when an entire row is
            // selected
            // (highlighted), cells in the row will go into edit mode - sort of
            // ?????
            spreadSheet.setEnabled(false);
            setToolBarBtnsEnabled(false);
            if (uploadToolPanel.isExpanded()) {
                hideUploadToolPanel();
                showHideUploadToolBtn.setToolTipText(getResourceString("WB_SHOW_UPLOADTOOLPANEL"));
                restoreUploadToolPanel = true;
            }
            if (imageFrame != null && imageFrame.isVisible()) {
                imageFrame.setVisible(false);
            }
            datasetUploader.startUI();
            if (doIncrementalValidation && invalidCellCount.get() == 0) {
                datasetUploader.validateData(true);
            }
        } catch (Exception ex) {
            edu.ku.brc.af.core.UsageTracker.incrHandledUsageCount();
            edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(WorkbenchPaneSS.class, ex);
            UIRegistry.getStatusBar().setErrorMessage(ex.getMessage());
            Uploader.unlockApp();
            Uploader.unlockUpload();
            uploadDone();
        } finally {
            setAllUploadDatasetBtnEnabled(canUpload());
        }
    }

    /**
     * Removes uploader ui and redisplays standard wb ui
     */
    public void uploadDone() {
        datasetUploader = null;
        Uploader.unlockApp();
        if (!Uploader.unlockUpload()) {
            log.error("unable to unlock upload task semaphore.");
            //inform the user??
        }
        if (uploadPane != null) {
            List<SubPaneIFace> badPanes = checkOpenTasksForUploadClose();
            if (badPanes.size() > 0) {
                for (SubPaneIFace badPane : badPanes) {
                    if (!SubPaneMgr.getInstance().removePane(badPane, true)) {
                        log.error("unable to close " + badPane.getClass().getName() + " after uploader close.");
                    }
                }
            }

            mainPanel.remove(uploadPane);
            mainPanel.add(spreadSheet.getScrollPane(), PanelType.Spreadsheet.toString());
            showPanel(PanelType.Spreadsheet);
            mainPanel.validate();
            mainPanel.doLayout();
        }
        ssFormSwitcher.setEnabled(true);
        spreadSheet.setEnabled(true);
        setToolBarBtnsEnabled(true);
        if (restoreUploadToolPanel) {
            showUploadToolPanel();
            showHideUploadToolBtn.setToolTipText(getResourceString("WB_HIDE_UPLOADTOOLPANEL"));
            restoreUploadToolPanel = false;
        }
        setAllUploadDatasetBtnEnabled(true);
    }

    /**
     * @param enabled
     * 
     * Enables buttons on the toolbar.
     * If enabled is false open forms associated with buttons are closed.
     */
    protected void setToolBarBtnsEnabled(boolean enabled) {
        if (deleteRowsBtn != null) {
            deleteRowsBtn.setEnabled(enabled);
        }
        if (clearCellsBtn != null) {
            clearCellsBtn.setEnabled(enabled);
        }
        if (addRowsBtn != null) {
            addRowsBtn.setEnabled(enabled);
        }
        if (carryForwardBtn != null) {
            carryForwardBtn.setEnabled(enabled);
        }
        if (toggleImageFrameBtn != null) {
            toggleImageFrameBtn.setEnabled(enabled);
        }
        boolean missingGeoRefFlds = getMissingGeoRefLatLonFields().length > 0;
        if (showMapBtn != null) {
            showMapBtn.setEnabled(enabled && !missingGeoRefFlds);
        }
        if (controlPropsBtn != null) {
            controlPropsBtn.setEnabled(enabled);
        }
        if (exportKmlBtn != null) {
            exportKmlBtn.setEnabled(enabled && !missingGeoRefFlds);
        }
        if (geoRefToolBtn != null) {
            geoRefToolBtn.setEnabled(enabled);
        }
        if (convertGeoRefFormatBtn != null) {
            if (!enabled) {
                if (this.geoRefConvertDlg != null) {
                    geoRefConvertDlg.setVisible(false);
                }
            }
            convertGeoRefFormatBtn.setEnabled(enabled && !missingGeoRefFlds);

        }
        if (exportExcelCsvBtn != null) {
            exportExcelCsvBtn.setEnabled(enabled);
        }

        if (showHideUploadToolBtn != null) {
            showHideUploadToolBtn.setEnabled(enabled);
        }

        for (JComponent btn : workBenchPluginSSBtns) {
            btn.setEnabled(enabled);
        }
    }

    protected void setAllUploadDatasetBtnEnabled(boolean enabled) {
        for (SubPaneIFace sp : SubPaneMgr.getInstance().getSubPanes()) {
            if (sp.getClass().equals(WorkbenchPaneSS.class)) {
                WorkbenchPaneSS wbSS = (WorkbenchPaneSS) sp;
                boolean canEnable = enabled && !wbSS.isChanged();
                wbSS.uploadDatasetBtn.setEnabled(canEnable);
                if (canEnable) {
                    wbSS.uploadDatasetBtn.setToolTipText(getResourceString("WB_UPLOAD_DATA"));
                } else {
                    wbSS.uploadDatasetBtn.setToolTipText(getResourceString("WB_UPLOAD_IN_PROGRESS"));
                }
            }
        }

    }

    /**
     * Updates uploadDatasetBtn wrt hasChanged
     */
    protected void updateUploadBtnState() {
        if (canUpload()) {
            if (hasChanged) {
                uploadDatasetBtn.setEnabled(false);
            } else {
                if (!doIncrementalValidation) {
                    uploadDatasetBtn.setEnabled(true);
                } else {
                    uploadDatasetBtn.setEnabled(invalidCellCount.get() == 0);
                }
            }
            if (uploadDatasetBtn.isEnabled()) {
                uploadDatasetBtn.setToolTipText(getResourceString("WB_UPLOAD_DATA"));
            } else if (doIncrementalValidation && invalidCellCount.get() > 0) {
                uploadDatasetBtn.setToolTipText(getResourceString("WB_UPLOAD_INVALID_DATA_HINT"));
            } else {
                uploadDatasetBtn.setToolTipText(getResourceString("WB_UPLOAD_UNSAVED_CHANGES_HINT"));
            }
        }
    }

    /**
     * @return true if current user has upload privileges
     */
    protected boolean isUploadPermitted() {
        return ContextMgr.getTaskByClass(WorkbenchTask.class).getPermissions().canModify();
    }

    /**
     * @return true if it is OK/possible to perform an upload.
     */
    protected boolean canUpload() {
        return datasetUploader == null && isUploadPermitted() && !UIRegistry.isMobile();
    }

    /**
     * @param stats list of cell stats for row
     * @param wbRow 
     * @return list of updated data items
     */
    protected Hashtable<Short, Short> updateCellStatuses(List<CellStatusInfo> stats, final WorkbenchRow wbRow) {
        Hashtable<Short, Short> exceptionalItems = new Hashtable<Short, Short>();
        if (stats != null && stats.size() > 0) {
            for (CellStatusInfo issue : stats) {
                for (Integer col : issue.getColumns()) {
                    if (col >= 0) {
                        WorkbenchDataItem wbItem = wbRow.getItems().get(col.shortValue());
                        if (wbItem == null) {
                            //need to force creation of empty wbItem for blank cell
                            wbItem = wbRow.setData("", col.shortValue(), false, true);
                        }
                        if (wbItem != null) {
                            exceptionalItems.put(col.shortValue(), issue.getStatus());
                            //WorkbenchDataItems can be updated by GridCellEditor or by background validation initiated at load time or after find/replace ops         
                            synchronized (wbItem) {
                                wbItem.setStatusText(issue.getStatusText());
                                if (wbItem.getEditorValidationStatus() != issue.getStatus()) {
                                    wbItem.setEditorValidationStatus(issue.getStatus());
                                    if (issue.getStatus() == WorkbenchDataItem.VAL_ERROR
                                            || issue.getStatus() == WorkbenchDataItem.VAL_ERROR_EDIT) {
                                        invalidCellCount.getAndIncrement();
                                    } else if (issue.getStatus() == WorkbenchDataItem.VAL_MULTIPLE_MATCH
                                            || issue.getStatus() == WorkbenchDataItem.VAL_NEW_DATA) {
                                        unmatchedCellCount.getAndIncrement();
                                    }

                                    //System.out.println("error " + invalidCellCount.get());
                                }
                            }
                        } else {
                            log.error("couldn't find workbench item for col " + col);
                        }
                    } else {
                        log.error(issue.getStatusText() + " at " + col + "???");
                    }
                }
            }
        }
        return exceptionalItems;
    }

    protected CellStatusInfo createDupCatNumEntryCellStatus(Integer badRow) {
        return new CellStatusInfo(badRow);
    }

    /**
     * @param editRow
     * @param editCol (use -1 to validate entire row)
     */
    protected void updateRowValidationStatus(int editRow, int editCol, Vector<Integer> badCats) {
        WorkbenchRow wbRow = workbench.getRow(editRow);
        List<UploadTableInvalidValue> issues = getIncremental() ? workbenchValidator.endCellEdit(editRow, editCol)
                : new Vector<UploadTableInvalidValue>();
        List<UploadTableMatchInfo> matchInfo = null;

        Hashtable<Short, Short> originalStats = new Hashtable<Short, Short>();
        Hashtable<Short, WorkbenchDataItem> originals = wbRow.getItems();
        for (Map.Entry<Short, WorkbenchDataItem> original : originals.entrySet()) {
            originalStats.put(original.getKey(), (short) original.getValue().getEditorValidationStatus());
        }

        //      if (workbenchValidator.getUploader().isUpdateUpload())
        //      {
        //         
        //      }

        if (doIncrementalMatching) {
            try {
                //XXX Really should avoid matching invalid columns. But that is tricky with trees.
                matchInfo = workbenchValidator.getUploader().matchData(editRow, editCol, issues);
            } catch (Exception ex) {
                //XXX what to do?, some exception might be caused by invalid data - filter out cols in exceptionalItems??
                //Maybe exceptions can be expected in general for a workbench-in-progress?
                //Or maybe we should blow up and force the workbench to close or something similarly drastic???
                ex.printStackTrace();
            }
        }

        List<CellStatusInfo> csis = new Vector<CellStatusInfo>(issues.size()
                + (matchInfo == null ? 0 : matchInfo.size()) + (badCats == null ? 0 : badCats.size()));
        for (UploadTableInvalidValue utiv : issues) {
            csis.add(new CellStatusInfo(utiv));
        }
        if (doIncrementalMatching && matchInfo != null) {
            for (UploadTableMatchInfo utmi : matchInfo) {
                if (utmi.getNumberOfMatches() != 1) //for now we don't care if a single match exists
                {
                    csis.add(new CellStatusInfo(utmi));
                }
            }
        }
        if (badCats != null) {
            for (Integer badCat : badCats) {
                csis.add(createDupCatNumEntryCellStatus(badCat));
            }
        }

        Hashtable<Short, Short> exceptionalItems = updateCellStatuses(csis, wbRow);
        for (WorkbenchDataItem wbItem : wbRow.getWorkbenchDataItems()) {
            Short origstat = originalStats.get(new Short((short) wbItem.getColumnNumber()));
            if (origstat != null) {
                if (origstat != wbItem.getEditorValidationStatus()
                        || exceptionalItems.get(wbItem.getColumnNumber()) == null) {
                    if (origstat == WorkbenchDataItem.VAL_MULTIPLE_MATCH
                            || origstat == WorkbenchDataItem.VAL_NEW_DATA) {
                        unmatchedCellCount.getAndDecrement();
                    } else if (origstat == WorkbenchDataItem.VAL_ERROR
                            || origstat == WorkbenchDataItem.VAL_ERROR_EDIT) {
                        invalidCellCount.getAndDecrement();
                    }
                    if (exceptionalItems.get(wbItem.getColumnNumber()) == null) {
                        //XXX synchronization is not really necessary anymore, right??
                        synchronized (wbItem) {
                            wbItem.setStatusText(null);
                            wbItem.setEditorValidationStatus(WorkbenchDataItem.VAL_OK);
                        }
                    }
                }
            }

        }
    }

    /**
     * @param startRow
     * @param endRow
     * return true if validating
     * 
     * Validates all rows between startRow and endRow
     * startRow and endRow are assumed to be model indices, 
     */
    protected boolean validateRows(final int startRow, final int endRow) {
        if (getIncremental()) {
            validateRows(null, startRow, endRow, true, null, false);
            return true;
        }
        return false;
    }

    /**
     * @param rows
     * return true if validating
     * 
     * Validates rows 
     * rows assumed to contain model indices, 
     */
    public boolean validateRows(final int[] rows) {
        if (getIncremental()) {
            validateRows(rows, -1, -1, rows.length <= 17, null, false);
            //validateRows(rows, -1, -1, true, null);
            return true;
        }
        return false;
    }

    /**
     * 
     */
    private void readRegisteries() {
        HashMap<String, WBPluginInfo> plugins = new HashMap<String, WorkbenchPaneSS.WBPluginInfo>();
        String fileName = "wb_registry.xml";

        String path = XMLHelper.getConfigDirPath(fileName);
        readRegistry(path, plugins);

        path = AppPreferences.getLocalPrefs().getDirPath() + File.separator + fileName;
        readRegistry(path, plugins);

        for (WBPluginInfo wbp : plugins.values()) {
            String prefName = wbp.getPrefName();
            if (StringUtils.isEmpty(prefName) || AppPreferences.getLocalPrefs().getBoolean(prefName, false)) {
                createPlugin(wbp.getClassName(), wbp.getIconName(), wbp.getToolTip());
            }
        }
    }

    /**
     * @param path
     * @param plugins
     */
    private void readRegistry(final String path, final HashMap<String, WBPluginInfo> plugins) {
        File file = new File(path);
        if (file.exists()) {
            try {
                Element root = XMLHelper.readFileToDOM4J(file);
                if (root != null) {
                    for (Iterator<?> i = root.elementIterator("plugin"); i.hasNext();) //$NON-NLS-1$
                    {
                        Element node = (Element) i.next();
                        String pluginName = node.attributeValue("name"); //$NON-NLS-1$
                        String className = node.attributeValue("class"); //$NON-NLS-1$
                        String iconName = node.attributeValue("icon"); //$NON-NLS-1$
                        String toolTip = node.attributeValue("tooltip"); //$NON-NLS-1$
                        String prefName = node.attributeValue("pref"); //$NON-NLS-1$

                        if (StringUtils.isNotEmpty(pluginName) && StringUtils.isNotEmpty(className)
                                && StringUtils.isNotEmpty(iconName)) {
                            WBPluginInfo wbp = new WBPluginInfo(pluginName, className, iconName, toolTip, prefName);
                            plugins.put(pluginName, wbp);

                        } else {
                            log.error("WBPlugin in error: One of the fields (name, class, icon) is null or empty.");
                        }
                    }
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @param className
     * @param iconName
     * @param tooltipKey
     */
    protected void createPlugin(final String className, final String iconName, final String tooltipKey) {
        try {
            final Class<?> wbPluginCls = Class.forName(className);
            final WorkBenchPluginIFace wbPlugin = (WorkBenchPluginIFace) wbPluginCls.newInstance();
            workBenchPlugins.put(wbPluginCls, wbPlugin);

            wbPlugin.setWorkbenchPaneSS(this);
            wbPlugin.setSpreadSheet(spreadSheet);
            wbPlugin.setWorkbench(workbench);

            workBenchPluginSSBtns.addAll(wbPlugin.getSSButtons());
            workBenchPluginFormBtns.addAll(wbPlugin.getFormButtons());

            //            List<String> missingFields = wbPlugin.getMissingFieldsForPlugin();
            //            if (missingFields != null && missingFields.size() > 0)
            //            {
            //                btn.setEnabled(false);
            //                String ttText = "<p>" + getResourceString("WB_ADDITIONAL_FIELDS_REQD") + ":<ul>";
            //                for (String reqdField : missingFields)
            //                {
            //                    ttText += "<li>" + reqdField + "</li>";
            //                }
            //                ttText += "</ul>";
            //                String origTT = btn.getToolTipText();
            //                btn.setToolTipText("<html>" + origTT + ttText);
            //            }
            //            else
            //            {
            //                btn.setEnabled(true);
            //            }
            //            
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public WorkBenchPluginIFace getPlugin(Class<?> cls) {
        return workBenchPlugins.get(cls);
    }

    //----------------------------------------------------------------------------------------
    //--
    //----------------------------------------------------------------------------------------
    private class ValidationWorker extends javax.swing.SwingWorker<Object, Object> {
        private final int[] rows;
        private final int startRow;
        private final int endRow;
        private final boolean useGlassPane;
        private SimpleGlassPane glassPane;
        private final boolean allowCancel;
        private final AtomicBoolean cancelledByUser = new AtomicBoolean(false);

        //Vectors are thread safe?? Right??
        //private final Vector<Integer> deletedRows = new Vector<Integer>();

        /**
        * @param rows
        * @param startRow
        * @param endRow
        */
        public ValidationWorker(int[] rows, int startRow, int endRow, boolean useGlassPane, boolean allowCancel) {
            super();
            this.rows = rows;
            this.startRow = startRow;
            this.endRow = endRow;
            this.useGlassPane = useGlassPane;
            this.allowCancel = allowCancel;
        }

        /**
         * @param row
         * @return row adjusted to account for deletes. Or -1 if the row has been deleted.
         */
        /*private int adjustRow(int row)
        {
           int result = row;
           //Not sure what happens if deletedRows is added to during the following loop.
           //Doesn't seem important enough to worry about.
           for (Integer deleted : deletedRows)
           {
        if (deleted == row)
        {
           result = -1;
           break;
               
        } else if (row > deleted)
        {
           result--;
        }
           } 
           return result;
        }*/

        /*
         * (non-Javadoc)
         * 
         * @see javax.swing.SwingWorker#doInBackground()
         */
        @Override
        protected Object doInBackground() throws Exception {
            if (useGlassPane) {
                this.glassPane = UIRegistry
                        .writeSimpleGlassPaneMsg(
                                String.format(getResourceString("WorkbenchPaneSS.Validating"),
                                        new Object[] { workbench.getName() }),
                                WorkbenchTask.GLASSPANE_FONT_SIZE, true);
                if (allowCancel) {
                    UIRegistry.displayStatusBarText(getResourceString("WorkbenchPaneSS.CancelValidationHint"));
                    this.glassPane.addMouseListener(new MouseListener() {

                        /*
                         * (non-Javadoc)
                         * 
                         * @see
                         * java.awt.event.MouseListener#mouseClicked(java.awt
                         * .event.MouseEvent)
                         */
                        @Override
                        public void mouseClicked(MouseEvent arg0) {
                            if (!arg0.isConsumed()) {
                                if (arg0.getClickCount() == 2) {
                                    SwingUtilities.invokeLater(new Runnable() {

                                        /* (non-Javadoc)
                                         * @see java.lang.Runnable#run()
                                         */
                                        @Override
                                        public void run() {
                                            if (UIRegistry.displayConfirmLocalized(
                                                    "WorkbenchPaneSS.CancelValidationConfirmTitle",
                                                    "WorkbenchPaneSS.CancelValidationConfirmMsg", "YES", "NO",
                                                    JOptionPane.QUESTION_MESSAGE)) {
                                                cancelledByUser.set(true);
                                            }
                                        }

                                    });
                                }
                            }
                        }

                        /*
                         * (non-Javadoc)
                         * 
                         * @see
                         * java.awt.event.MouseListener#mouseEntered(java.awt
                         * .event.MouseEvent)
                         */
                        @Override
                        public void mouseEntered(MouseEvent arg0) {
                            // TODO Auto-generated method stub

                        }

                        /*
                         * (non-Javadoc)
                         * 
                         * @see
                         * java.awt.event.MouseListener#mouseExited(java.awt
                         * .event.MouseEvent)
                         */
                        @Override
                        public void mouseExited(MouseEvent arg0) {
                            // TODO Auto-generated method stub

                        }

                        /*
                         * (non-Javadoc)
                         * 
                         * @see
                         * java.awt.event.MouseListener#mousePressed(java.awt
                         * .event.MouseEvent)
                         */
                        @Override
                        public void mousePressed(MouseEvent arg0) {
                            // TODO Auto-generated method stub

                        }

                        /*
                         * (non-Javadoc)
                         * 
                         * @see
                         * java.awt.event.MouseListener#mouseReleased(java.awt
                         * .event.MouseEvent)
                         */
                        @Override
                        public void mouseReleased(MouseEvent arg0) {
                            // TODO Auto-generated method stub

                        }

                    });
                }
            }
            boolean checkedCatNums = catNumChecker == null;
            if (rows != null) {
                int count = rows.length;
                int rowCount = 0;
                for (int row : rows) {
                    if (cancelledByUser.get()) {
                        break;
                    }

                    //int adjustedRow = adjustRow(row);
                    if (row != -1) {

                        if (!checkedCatNums) {
                            Vector<Integer> badCats = catNumChecker.checkValues(rows);
                            updateRowValidationStatus(row, -1, badCats);
                            checkedCatNums = true;
                        } else {
                            updateRowValidationStatus(row, -1, null);
                        }
                    }
                    if (useGlassPane) {
                        //System.out.println((int)( (100.0 * ++rowCount) / count));
                        glassPane.setProgress((int) ((100.0 * ++rowCount) / count));
                    }
                }
            } else {
                int count = endRow - startRow + 1;
                int rowCount = 0;
                try {
                    for (int row = startRow; row <= endRow; row++) {
                        if (cancelledByUser.get()) {
                            break;
                        }
                        //int adjustedRow = adjustRow(row);
                        if (row != -1) {
                            if (!checkedCatNums) {
                                Vector<Integer> badCats = catNumChecker.checkValues(rows);
                                updateRowValidationStatus(row, -1, badCats);
                                checkedCatNums = true;
                            } else {
                                updateRowValidationStatus(row, -1, null);
                            }
                        }
                        if (useGlassPane) {
                            int progress = (int) ((100.0 * ++rowCount) / count);
                            //System.out.println(progress);
                            glassPane.setProgress(progress);
                        }
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (!isCancelled()) {
                //System.out.println("done(): remove current validationWorker");
                validationWorkerQueue.remove(); //remove this worker

                if (validationWorkerQueue.peek() != null) {
                    //System.out.println("done(): executing next validationWorker");
                    validationWorkerQueue.peek().execute();
                }
            }
            return null;
        }

        /* (non-Javadoc)
         * @see javax.swing.SwingWorker#done()
         */
        @Override
        protected void done() {
            super.done();
            UIRegistry.displayStatusBarText(null);
            if (useGlassPane) {
                UIRegistry.clearSimpleGlassPaneMsg();
            }
            if (isCancelled()) {
                //currently cancellation only occurs during shutdown.
                //System.out.println("done(): Clearing validationWorkerQueue");
                validationWorkerQueue.clear();
            }
            if (cancelledByUser.get()) {
                //XXX need to verify cancel. Inside the doInBackground loop.
                turnOffIncrementalValidation();
                turnOffIncrementalMatching();
                uploadToolPanel.uncheckAutoMatching();
                uploadToolPanel.uncheckAutoValidation();
                uploadToolPanel.updateBtnUI();
            }

            //         System.out.println("done(): remove current validationWorker");
            //         validationWorkerQueue.remove(); //remove this worker
            //         
            //         if (validationWorkerQueue.peek() != null)
            //         {
            //            System.out.println("done(): executing next validationWorker");
            //            validationWorkerQueue.peek().execute();
            //         }

            if (validationWorkerQueue.peek() == null) {
                SwingUtilities.invokeLater(new Runnable() {

                    @Override
                    public void run() {
                        updateBtnUI();
                    }

                });
            }

            if (isCancelled()) {
                return;
            }

            boolean savedBlockChanges = blockChanges;
            try {
                blockChanges = true;
                if (rows == null) {
                    model.fireTableRowsUpdated(startRow, endRow); //XXX model vs table rows??
                } else {
                    model.fireDataChanged();
                }
            } finally {
                blockChanges = savedBlockChanges;
            }
        }

        //      public void rowDeleted(int row)
        //      {
        //         deletedRows.add(row);
        //      }
    }

    /**
     * @param glassPane
     */
    public void validateAll(final SimpleGlassPane glassPane) {
        //System.out.println("validating all " + spreadSheet.getRowCount() + " rows.");
        if (catNumChecker != null) {
            //apparently this is pretty quick, but it might be necessary to have a glass pane for this step...
            catNumChecker.clear();
            for (int r = 0; r < spreadSheet.getRowCount(); r++) {
                catNumChecker.setValue(r, spreadSheet.getStringAt(r, catNumCol), false);
            }
        }
        validateRows(null, 0, spreadSheet.getRowCount() - 1, false, glassPane, glassPane == null);
    }

    /**
     * @param rows
     * @param startRow
     * @param endRow
     * @param isDoInBackground
     */
    protected void validateRows(final int[] rows, final int startRow, final int endRow, boolean doSecretly,
            final SimpleGlassPane glassPane, final boolean allowCancel) {

        ValidationWorker newWorker = new ValidationWorker(rows, startRow, endRow, !doSecretly, allowCancel);
        //       if (doSecretly)
        {
            //System.out.println("validateRows(): adding worker to queue");
            validationWorkerQueue.add(newWorker);
            if (validationWorkerQueue.peek() == newWorker) {
                //System.out.println("validateRows(): executing new worker");
                newWorker.execute();
            }
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    addRowsBtn.setEnabled(false);
                    deleteRowsBtn.setEnabled(false);
                }
            });
        } //else
        //       if (!doSecretly)
        //       {
        //          try
        //          {
        //             //newWorker.execute();
        //             newWorker.get();
        //          } catch (Exception ex)
        //          {
        //                UsageTracker.incrHandledUsageCount();
        //                edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(WorkbenchPaneSS.class, ex);
        //                log.error(ex);
        //          }
        //       }
    }

    /**
     * @param col
     * @return true if the field mapped to col has an incrementing formatter.
     */
    public UIFieldFormatterIFace getFormatterForCol(int col) {
        if (workbenchValidator != null && workbenchValidator.getUploader() != null) {
            DBFieldInfo fldInfo = workbenchValidator.getUploader().getFieldInfoForCol(col);
            if (fldInfo != null) {
                return fldInfo.getFormatter();
            }
        }
        return null;
    }

    /**
     * @param startRow
     * @param endRow
     * @param rows
     * @param check
     * @return
     */
    protected List<Integer> setCatNumValues(int startRow, int endRow, int[] rows, boolean check) {
        List<Integer> result = check ? new Vector<Integer>() : null;
        if (rows == null) {
            for (int row = startRow; row <= endRow; row++) {
                if (check) {
                    result.addAll(catNumChecker.setValue(row, spreadSheet.getStringAt(row, catNumCol), check));
                } else {
                    catNumChecker.setValue(row, spreadSheet.getStringAt(row, catNumCol), check);
                }
            }
        } else {
            for (int row : rows) {
                if (check) {
                    result.addAll(catNumChecker.setValue(row, spreadSheet.getStringAt(row, catNumCol), check));
                } else {
                    catNumChecker.setValue(row, spreadSheet.getStringAt(row, catNumCol), check);
                }

            }
        }
        return result;
    }

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

    public class GridCellEditor extends DefaultCellEditor implements TableCellEditor//, UndoableTextIFace
    {
        protected JComponent uiComponent;
        protected int length;
        protected LengthInputVerifier verifier;
        protected JButton ceSaveBtn;
        protected DocumentListener docListener;
        protected int editCol = -1;
        protected int editRow = -1;
        //protected UndoManager undoManager = new UndoManager();

        public GridCellEditor(final JTextField textField, final String caption, final int length,
                final JButton gcSaveBtn) {
            super(textField);
            init(textField, caption, length, gcSaveBtn);
        }

        public GridCellEditor(final JComboBox<?> combo, final String caption, final int length,
                final JButton gcSaveBtn) {
            super(combo);
            init(combo, caption, length, gcSaveBtn);
        }

        protected void init(final JComponent comp, final String caption, final int length,
                final JButton gcSaveBtn) {
            this.uiComponent = comp;
            this.length = length;
            this.ceSaveBtn = saveBtn;

            //verifier = new LengthInputVerifier(caption, length);
            //uiComponent.setInputVerifier(verifier);

            uiComponent.setBorder(BorderFactory.createLineBorder(Color.BLACK));
            //            docListener = new DocumentListener() {
            //                public void changedUpdate(DocumentEvent e)
            //                {
            //                    validateDoc();
            //                }
            //                
            //                public void insertUpdate(DocumentEvent e)
            //                {
            //                    validateDoc();
            //                }
            //                
            //                public void removeUpdate(DocumentEvent e) 
            //                {
            //                    validateDoc();
            //                }
            //            };
            //textField.getDocument().addDocumentListener(docListener);
        }

        /**
         * Makes sure the document is the correct length.
         */
        protected void validateDoc() {
            if (!verifier.verify(uiComponent)) {
                ceSaveBtn.setEnabled(false);
            }
        }

        /**
         * clean up after processing stopCellEditing
         */
        protected void endStopCellEditProcessing() {
            editCol = -1;
            editRow = -1;
        }

        /* (non-Javadoc)
         * @see javax.swing.DefaultCellEditor#stopCellEditing()
         */
        @Override
        public boolean stopCellEditing() {
            boolean result = super.stopCellEditing();
            if (editRow == -1 || editCol == -1) {
                editRow = -1;
                editCol = -1;
                return result; //a 'superfluous' re-call of this method.
            }
            if (result) {
                if (verifier != null && !verifier.verify(uiComponent)) {
                    ceSaveBtn.setEnabled(false);
                    editRow = -1;
                    editCol = -1;
                    return false;
                }
                if (getIncremental() && workbenchValidator != null) {
                    Vector<Integer> badCats = null;
                    if (catNumChecker != null && editCol == catNumCol) {
                        badCats = catNumChecker.setValue(editRow, ((JTextField) uiComponent).getText(), true);
                    }
                    updateRowValidationStatus(spreadSheet.convertRowIndexToModel(editRow),
                            spreadSheet.convertColumnIndexToModel(editCol), badCats);
                    updateBtnUI();
                }
                endStopCellEditProcessing();
            }
            return result;
        }

        /* (non-Javadoc)
         * @see javax.swing.DefaultCellEditor#cancelCellEditing()
         */
        @Override
        public void cancelCellEditing() {
            editRow = -1;
            editCol = -1;
            super.cancelCellEditing();
        }

        /* (non-Javadoc)
           * @see javax.swing.CellEditor#getCellEditorValue()
           */
        @Override
        public Object getCellEditorValue() {
            return ((JTextField) uiComponent).getText();
        }

        /* (non-Javadoc)
         * @see javax.swing.AbstractCellEditor#isCellEditable(java.util.EventObject)
         */
        @Override
        public boolean isCellEditable(EventObject e) {
            return true;
        }

        //
        //          Implementing the CellEditor Interface
        //

        /* (non-Javadoc)
         * @see javax.swing.DefaultCellEditor#getTableCellEditorComponent(javax.swing.JTable, java.lang.Object, boolean, int, int)
         */
        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row,
                int column) {
            // This is needed for Mac OS X, not Linux and I am not sure about Windows
            // If the fonts aren't the same then the double click doesn't find the correct
            // storage to insert the cursor
            if (table.getCellRenderer(row, column) instanceof JComponent) {
                JComponent jcomp = (JComponent) table.getCellRenderer(row, column);
                Font cellFont = jcomp.getFont();
                Font txtFont = uiComponent.getFont();
                if (cellFont != txtFont) {
                    uiComponent.setFont(cellFont);
                }
            }

            ((JTextField) uiComponent).setText(value != null ? value.toString() : "");
            try {
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        Caret c = ((JTextField) uiComponent).getCaret();

                        // for keyboard
                        c.setVisible(true);
                        c.setSelectionVisible(true);
                        //textField.requestFocus();
                    }
                });
            } catch (Exception e) {
                // ignore it?
            }
            editCol = column;
            editRow = row;
            return uiComponent;
        }

        /* (non-Javadoc)
         * @see edu.ku.brc.ui.UIRegistry.UndoableTextIFace#getUndoManager()
         */
        public UndoManager getUndoManager() {
            return null;//undoManager;
        }

        /* (non-Javadoc)
         * @see edu.ku.brc.ui.UIRegistry.UndoableTextIFace#getText()
         */
        public JTextComponent getTextComponent() {
            return (JTextComponent) uiComponent;
        }

        /**
         * cleans up listeners etc.
         */
        public void cleanUp() {
            uiComponent.setInputVerifier(null);
            //textField.getDocument().removeDocumentListener(docListener);
            uiComponent = null;
            verifier = null;
            ceSaveBtn = null;
        }
    }

    /**
     * @author timo
     *
     *Cell Editor for pick lists (and possibly lookups)
     */
    public class GridCellListEditor extends GridCellEditor {
        /**
         * @param combo
         * @param caption
         * @param length
         * @param gcSaveBtn
         */
        public GridCellListEditor(final JComboBox<?> combo, final String caption, final int length,
                final JButton gcSaveBtn) {
            super(combo, caption, length, gcSaveBtn);
            //model = new DefaultComboBoxModel(pickList.getList());
            //combo.setModel(model);
        }

        /* (non-Javadoc)
         * @see edu.ku.brc.specify.tasks.subpane.wb.WorkbenchPaneSS.GridCellEditor#getTableCellEditorComponent(javax.swing.JTable, java.lang.Object, boolean, int, int)
         */
        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row,
                int column) {
            editCol = column;
            editRow = row;
            ((JComboBox<?>) uiComponent).setSelectedItem(value);
            return uiComponent;
        }

        /* (non-Javadoc)
           * @see javax.swing.CellEditor#getCellEditorValue()
           */
        @Override
        public Object getCellEditorValue() {
            return ((JComboBox<?>) uiComponent).getSelectedItem().toString();
        }

        /* (non-Javadoc)
         * @see edu.ku.brc.specify.tasks.subpane.wb.WorkbenchPaneSS.GridCellEditor#endStopCellEditProcessing()
         */
        @Override
        protected void endStopCellEditProcessing() {
            //don't do nuthin.       
        }

        /**
         * @return the model
         */
        public ComboBoxModel<?> getList() {
            return ((JComboBox<?>) uiComponent).getModel();
        }

    }

    //------------------------------------------------------------
    // Switches between the Grid View and the Form View
    //------------------------------------------------------------
    class SwitcherAL implements ActionListener {
        protected DropDownButtonStateful switcherComp;

        public SwitcherAL(final DropDownButtonStateful switcherComp) {
            this.switcherComp = switcherComp;
        }

        public void actionPerformed(ActionEvent ae) {
            showPanel(((DropDownButtonStateful) ae.getSource()).getCurrentIndex() == 0 ? PanelType.Spreadsheet
                    : PanelType.Form);
        }
    }

    /**
     * @author timo
     *
     *Renderer for workbench cells that checks cells validation status and status text.
     */
    public class WbCellRenderer extends DefaultTableCellRenderer {
        /* (non-Javadoc)
         * @see javax.swing.table.DefaultTableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
         */
        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
                boolean hasFocus, int tblRow, int tblColumn) {
            JLabel lbl = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, tblRow,
                    tblColumn);

            int modelRow = spreadSheet.convertRowIndexToModel(tblRow);
            WorkbenchRow wbRow = workbench.getRow(modelRow);
            String cardImageFullPath = wbRow.getCardImageFullPath();
            if (cardImageFullPath != null) {
                String filename = FilenameUtils.getBaseName(cardImageFullPath);
                filename = FilenameUtils.getName(cardImageFullPath);
                lbl.setText(filename);
                lbl.setHorizontalAlignment(SwingConstants.CENTER);
            }
            return lbl;
        }
    }

    /**
     * @author timo
     * 
     * Column header renderer that adds icon for the specify table that contains the field the column is mapped to.
     *
     */
    public class WbTableHeaderRenderer implements TableCellRenderer {
        protected JLabel iconLbl;
        protected JPanel panel;
        protected DefaultTableCellHeaderRenderer header;

        /**
         * 
         */
        public WbTableHeaderRenderer(final String tableName) {
            super();

            ImageIcon icon = tableName != null ? IconManager.getIcon(tableName, IconManager.IconSize.Std16) : null;
            iconLbl = UIHelper.createLabel(null, icon);
            header = new DefaultTableCellHeaderRenderer();
            header.setHorizontalTextPosition(JLabel.LEFT);

            CellConstraints cc = new CellConstraints();
            PanelBuilder pb = new PanelBuilder(new FormLayout("p,1px,f:p:g", "f:p:g"));
            pb.add(iconLbl, cc.xy(1, 1));
            pb.add(header, cc.xy(3, 1));
            panel = pb.getPanel();
            panel.setOpaque(false);
        }

        /* (non-Javadoc)
         * @see javax.swing.table.TableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
         */
        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
                boolean hasFocus, int rowIndex, int vColIndex) {
            header.getTableCellRendererComponent(table, value, isSelected, hasFocus, rowIndex, vColIndex);
            header.setOpaque(false);
            //header.setBackground(Color.BLUE);
            return panel;
        }

        //public void validate() {}
        //public void revalidate() {}
        //protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {}
        //public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {}
    }

    //------------------------------------------------------------
    // TableCellRenderer for showing the proper Icon when there 
    // is an image column (CardImage)
    //------------------------------------------------------------
    public class ImageRenderer extends DefaultTableCellRenderer {
        protected Border selectedBorder = BorderFactory.createMatteBorder(1, 1, 1, 1, new Color(99, 130, 191));// Blue-Border

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
                boolean hasFocus, int row, int column) {
            setText("");
            if (value instanceof ImageIcon) {
                setIcon((ImageIcon) value);
                this.setHorizontalAlignment(SwingConstants.CENTER);
            } else {
                setIcon(null);
            }
            if (isSelected) {
                setBackground(table.getSelectionBackground());
            } else {
                setBackground(table.getBackground());
            }
            if (hasFocus) {
                setBorder(selectedBorder);
            } else {
                setBorder(null);
            }
            return this;
        }
    }

    public class CellPosition extends Pair<Integer, Integer> {
        public CellPosition(final int row, final int col) {
            super(row, col);
        }

        @Override
        public String toString() {
            return UIRegistry.getResourceString("WB_ROW") + ": " + (getFirst() + 1) + ", "
                    + UIRegistry.getResourceString("WB_COLUMN") + ": " + (getSecond() + 1);
        }
    }

    public class ColumnHeaderListener extends MouseAdapter {
        @Override
        public void mousePressed(MouseEvent evt) {
            if (spreadSheet != null && spreadSheet.getCellEditor() != null) {
                spreadSheet.getCellEditor().stopCellEditing();
            }
            /* this code shows how to get what column was clicked on
             * 
            JTable table = ((JTableHeader) evt.getSource()).getTable();
            TableColumnModel colModel = table.getColumnModel();
                
            // The index of the column whose header was clicked
            int vColIndex = colModel.getColumnIndexAtX(evt.getX());
            int mColIndex = table.convertColumnIndexToModel(vColIndex);
                
            // Return if not clicked on any column header
            if (vColIndex == -1) { return; }
                
            // Determine if mouse was clicked between column heads
            Rectangle headerRect = table.getTableHeader().getHeaderRect(vColIndex);
            if (vColIndex == 0)
            {
            headerRect.width -= 3; // Hard-coded constant
            } else
            {
            headerRect.grow(-3, 0); // Hard-coded constant
            }
            if (!headerRect.contains(evt.getX(), evt.getY()))
            {
            // Mouse was clicked between column heads
            // vColIndex is the column head closest to the click
                
            // vLeftColIndex is the column head to the left of the click
            int vLeftColIndex = vColIndex;
            if (evt.getX() < headerRect.x)
            {
                vLeftColIndex--;
            }
            spreadSheet.getCellEditor().stopCellEditing();
            System.out.println(vLeftColIndex);
            }*/
        }
    }

    /**
     * @param hasChanged the hasChanged to set
     */
    public void setHasChanged(boolean hasChanged) {
        this.hasChanged = hasChanged;
    }

    /**
     * @return the spreadSheet
     */
    public SpreadSheet getSpreadSheet() {
        return spreadSheet;
    }

    /**
     * @param col
     * @return the max width for column indexed by col.
     */
    public Integer getColumnMaxWidth(int col) {
        return this.columnMaxWidths[col];
    }

    /**
     * @return
     */
    public CellRenderingAttributes getCellDecorator() {
        return cellRenderAtts;
    }

    /**
     * A debugging tool Used to find discrepancies between workbench and specify schemas.
     */
    protected void compareSchemas() {
        List<Pair<DBFieldInfo, DBFieldInfo>> badFlds = new LinkedList<Pair<DBFieldInfo, DBFieldInfo>>();
        DBTableIdMgr wbSchema = WorkbenchTask.getDatabaseSchema();
        for (DBTableInfo wbTbl : wbSchema.getTables()) {
            DBTableInfo tbl = DBTableIdMgr.getInstance().getInfoByTableName(wbTbl.getName());
            if (tbl != null) {
                for (DBFieldInfo wbFld : wbTbl.getFields()) {
                    DBFieldInfo fld = tbl.getFieldByName(wbFld.getName());
                    if (fld == null && wbFld.getName().contains("emarks")) {
                        fld = tbl.getFieldByName("remarks");
                    }
                    if (fld == null && wbFld.getName().matches(".*Text[1|2|3|4|5|6|7|8|9][0|1|2|3|4|5|6|7|8|9]*")) {
                        fld = tbl.getFieldByName("text" + wbFld.getName()
                                .substring(wbFld.getName().indexOf("Text", wbFld.getName().length() - 6) + 4));
                    }
                    if (fld == null && wbFld.getName().matches(".*YesNo[1|2|3|4|5|6|7|8|9]")) {
                        fld = tbl.getFieldByName("yesNo" + wbFld.getName().substring(wbFld.getName().length() - 1));
                    }
                    if (fld == null && wbFld.getName().matches(".*Number[1|2|3|4|5|6|7|8|9]")) {
                        fld = tbl
                                .getFieldByName("number" + wbFld.getName().substring(wbFld.getName().length() - 1));
                    }
                    if (fld != null) {
                        if (fld.getLength() != -1 && fld.getLength() != wbFld.getLength()) {
                            badFlds.add(new Pair<DBFieldInfo, DBFieldInfo>(fld, wbFld));
                        }
                    } else {
                        System.out.println("couldn't find field: " + wbTbl.getName() + "." + wbFld.getName());
                    }
                }
            } else {
                System.out.println("couldn't find table: " + wbTbl.getName());
            }
        }
        if (badFlds.size() > 0) {
            System.out.println("Change the lengths for the following fields...");
            for (Pair<DBFieldInfo, DBFieldInfo> badFld : badFlds) {
                DBFieldInfo fld = badFld.getFirst();
                DBFieldInfo wbFld = badFld.getSecond();
                System.out.println(fld.getTableInfo().getName() + "." + fld.getName() + "("
                        + wbFld.getTableInfo().getName() + "." + wbFld.getName() + ") - " + fld.getLength());
            }
        } else {
            System.out.println("All field lengths are OK.");
        }
    }

    public void incShutdownLock() {
        shutdownLock.incrementAndGet();
    }

    public void decShutdownLock() {
        shutdownLock.decrementAndGet();
    }

    /**
     * @return list tables in the workbench that support attachments
     */
    public List<UploadTable> getAttachableTables() {
        if (workbenchValidator == null) {
            buildValidator(true);
        }
        if (workbenchValidator != null) {
            return workbenchValidator.getUploader().getAttachableTablesInUse();
        }
        return null;
    }

    /**
     * @return the doIncrementalValidation
     */
    public boolean isDoIncremental() {
        return getIncremental();
    }

    protected List<Integer> getCatNumCol() {
        if (workbenchValidator != null) {

        }
        return null;
    }

    public FormPaneWrapper getFormPane() {
        return formPane;
    }

    /**
     * @author timo
     *
     */
    private class CellStatusInfo {
        protected final short status;
        protected final String statusText;
        protected final List<Integer> columns;

        /**
         * @param invalidValue
         */
        public CellStatusInfo(UploadTableInvalidValue invalidValue) {
            if (invalidValue.isWarn()) {
                //XXX this works for now only because isWarn is only true when non-readonly picklists don't contain values
                status = WorkbenchDataItem.VAL_NEW_DATA;
            } else {
                status = WorkbenchDataItem.VAL_ERROR;
            }
            statusText = invalidValue.getDescription();
            columns = invalidValue.getCols();
        }

        /**
         * @param matchInfo
         */
        public CellStatusInfo(UploadTableMatchInfo matchInfo) {
            if (matchInfo.isSkipped()) {
                status = WorkbenchDataItem.VAL_NOT_MATCHED;
            } else {
                //Currently, getNumberOfMatches() will never return 1
                status = matchInfo.getNumberOfMatches() == 0 ? WorkbenchDataItem.VAL_NEW_DATA
                        : WorkbenchDataItem.VAL_MULTIPLE_MATCH;
            }
            statusText = matchInfo.getDescription();
            columns = matchInfo.getColIdxs();
        }

        public CellStatusInfo(Integer dupCatNumRow) {
            status = WorkbenchDataItem.VAL_ERROR;
            statusText = UIRegistry.getResourceString("WorkbenchPaneSS.DupCatNumEntry");
            columns = new Vector<Integer>(1);
            columns.add(catNumCol);
        }

        /**
         * @return the status
         */
        public short getStatus() {
            return status;
        }

        /**
         * @return the statusText
         */
        public String getStatusText() {
            return statusText;
        }

        /**
         * @return the columns
         */
        public List<Integer> getColumns() {
            return columns;
        }
    }

    /**
     * @author timo
     *
     */
    private class GridCellPredicate implements HighlightPredicate {
        final static public int ValidationPredicate = 0;
        final static public int MatchingPredicate = 1;
        final static public int AnyPredicate = 2;
        protected final int activation;
        protected final Short[] conditions;

        public GridCellPredicate(int activation, Short[] conditions) {
            this.activation = activation;
            this.conditions = conditions;
        }

        /* (non-Javadoc)
         * @see org.jdesktop.swingx.decorator.HighlightPredicate#isHighlighted(java.awt.Component, org.jdesktop.swingx.decorator.ComponentAdapter)
         */
        @Override
        public boolean isHighlighted(Component arg0, ComponentAdapter arg1) {
            if ((activation == AnyPredicate && !getIncremental())
                    || (activation == ValidationPredicate && !doIncrementalValidation)
                    || (activation == MatchingPredicate && !doIncrementalMatching)) {
                return false;

            }

            WorkbenchRow wbRow = workbench.getRow(spreadSheet.convertRowIndexToModel(arg1.row));
            WorkbenchDataItem wbCell = wbRow.getItems()
                    .get((short) spreadSheet.convertColumnIndexToModel(arg1.column));
            if (wbCell == null) {
                return false;
            }

            int status = wbCell.getEditorValidationStatus();
            if (activation == AnyPredicate) {
                ((JLabel) arg0).setToolTipText(null);
                //Seems like a good idea to try to be as efficient as possible
                //but this will need to be recoded as new cell states are added
                return status == WorkbenchDataItem.VAL_ERROR || status == WorkbenchDataItem.VAL_ERROR_EDIT
                        || status == WorkbenchDataItem.VAL_MULTIPLE_MATCH
                        || status == WorkbenchDataItem.VAL_NEW_DATA || status == WorkbenchDataItem.VAL_NOT_MATCHED;
            } else {
                for (Short condition : conditions) {
                    if (condition == status) {
                        //System.out.println("pos: " + arg1.row + ", " + arg1.column + ": " + wbCell.getStatusText());
                        ((JLabel) arg0).setToolTipText(wbCell.getStatusText());
                        return true;
                    }
                }
            }
            return false;
        }
    }

    /**
     * @author timo
     *
     */
    private class GridCellHighlighter extends AbstractHighlighter {
        public GridCellHighlighter(HighlightPredicate predicate) {
            super(predicate);
        }

        /* (non-Javadoc)
         * @see org.jdesktop.swingx.decorator.AbstractHighlighter#doHighlight(java.awt.Component, org.jdesktop.swingx.decorator.ComponentAdapter)
         */
        @Override
        protected Component doHighlight(Component arg0, ComponentAdapter arg1) {
            WorkbenchRow wbRow = workbench.getRow(spreadSheet.convertRowIndexToModel(arg1.row));
            WorkbenchDataItem wbCell = wbRow.getItems()
                    .get((short) spreadSheet.convertColumnIndexToModel(arg1.column));
            cellRenderAtts.addAttributes((JLabel) arg0, wbCell, doIncrementalValidation, doIncrementalMatching);
            return arg0;
        }
    }

    //------------------------------------------------------------------------------------------------------
    //-- 
    //------------------------------------------------------------------------------------------------------

    class WBImageTransferable extends ImageTransferable {
        /* (non-Javadoc)
         * @see edu.ku.brc.specify.tasks.subpane.wb.ImageTransferable#processImages(java.util.Vector)
         */
        @Override
        protected void processImages(final Vector<File> fileList) {

            final SwingWorker worker = new SwingWorker() {
                protected boolean isOK = false;

                @Override
                public Object construct() {
                    // import the images into the Workbench, creating new rows (and saving the WB if it is brand new)
                    isOK = WorkbenchTask.importImages(workbench, fileList, WorkbenchPaneSS.this, false,
                            imageImportFrame.isOneImagePerRow());

                    return null;
                }

                @SuppressWarnings("synthetic-access")
                @Override
                public void finished() {
                    UIRegistry.clearGlassPaneMsg();

                    if (isOK) {
                        setChanged(true);

                        setImageFrameVisible(true);

                        // scrolls to the last row
                        newImagesAdded();
                    }
                }
            };
            worker.start();

        }
    }

    //------------------------------------------------------------------------------------------------------
    //-- 
    //------------------------------------------------------------------------------------------------------
    class WBPluginInfo {
        private String pluginName;
        private String className;
        private String iconName;
        private String toolTip;
        private String prefName;

        /**
         * @param pluginName
         * @param className
         * @param iconName
         * @param toolTip
         * @param prefName
         */
        public WBPluginInfo(final String pluginName, final String className, final String iconName,
                final String toolTip, final String prefName) {
            super();
            this.pluginName = pluginName;
            this.className = className;
            this.iconName = iconName;
            this.toolTip = toolTip;
            this.prefName = prefName;
        }

        /**
         * @return the pluginName
         */
        public String getPluginName() {
            return pluginName;
        }

        /**
         * @return the className
         */
        public String getClassName() {
            return className;
        }

        /**
         * @return the toolTip
         */
        public String getToolTip() {
            return toolTip;
        }

        /**
         * @return the prefName
         */
        public String getPrefName() {
            return prefName;
        }

        /**
         * @return the iconName
         */
        public String getIconName() {
            return iconName;
        }

    }
}