org.tellervo.desktop.tridasv2.ui.TridasMetadataPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.tellervo.desktop.tridasv2.ui.TridasMetadataPanel.java

Source

/*******************************************************************************
 * Copyright (C) 2010 Lucas Madar and Peter Brewer
 * 
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Lucas Madar
 *     Peter Brewer
 ******************************************************************************/
package org.tellervo.desktop.tridasv2.ui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Image;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;

import org.tellervo.desktop.core.App;
import org.tellervo.desktop.editor.AbstractMetadataPanel;
import org.tellervo.desktop.gui.BugDialog;
import org.tellervo.desktop.io.Metadata;
import org.tellervo.desktop.sample.Sample;
import org.tellervo.desktop.sample.SampleType;
import org.tellervo.desktop.tridasv2.LabCode;
import org.tellervo.desktop.tridasv2.LabCodeFormatter;
import org.tellervo.desktop.tridasv2.TridasCloner;
import org.tellervo.desktop.tridasv2.TridasComparator;
import org.tellervo.desktop.tridasv2.ui.support.ScrollableJButtonBar;
import org.tellervo.desktop.tridasv2.ui.support.TridasEntityDeriver;
import org.tellervo.desktop.tridasv2.ui.support.TridasEntityListHolder;
import org.tellervo.desktop.tridasv2.ui.support.TridasEntityProperty;
import org.tellervo.desktop.ui.Builder;
import org.tellervo.desktop.ui.FilterableComboBoxModel;
import org.tellervo.desktop.ui.I18n;
import org.tellervo.desktop.wsi.tellervo.TellervoResourceAccessDialog;
import org.tellervo.desktop.wsi.tellervo.resources.EntityResource;
import org.tellervo.schema.TellervoRequestType;
import org.tridas.interfaces.ITridas;
import org.tridas.interfaces.ITridasSeries;
import org.tridas.schema.ComplexPresenceAbsence;
import org.tridas.schema.PresenceAbsence;
import org.tridas.schema.TridasBark;
import org.tridas.schema.TridasDerivedSeries;
import org.tridas.schema.TridasElement;
import org.tridas.schema.TridasHeartwood;
import org.tridas.schema.TridasLastRingUnderBark;
import org.tridas.schema.TridasMeasurementSeries;
import org.tridas.schema.TridasObject;
import org.tridas.schema.TridasPith;
import org.tridas.schema.TridasProject;
import org.tridas.schema.TridasRadius;
import org.tridas.schema.TridasSample;
import org.tridas.schema.TridasSapwood;
import org.tridas.schema.TridasWoodCompleteness;
import org.tridas.util.TridasObjectEx;

import com.l2fprod.common.propertysheet.Property;
import com.l2fprod.common.propertysheet.PropertySheet;
import com.l2fprod.common.propertysheet.PropertySheetTableModel;
import com.l2fprod.common.swing.JButtonBar;
import com.l2fprod.common.swing.plaf.blue.BlueishButtonBarUI;
import com.lowagie.text.Font;

@SuppressWarnings("serial")
public class TridasMetadataPanel extends AbstractMetadataPanel implements PropertyChangeListener {
    /** The sample we're working on */
    private Sample sample;
    /** Our property sheet panel (contains table and description) */
    private TellervoPropertySheetPanel propertiesPanel;
    /** Our properties table */
    private TellervoPropertySheetTable propertiesTable;
    /** The sidebar that has buttons for sample, series, etc */
    private JButtonBar buttonBar;
    /** The current entity selected */
    private EditType currentMode;

    /** A copy of the entity that we're currently editing */
    private ITridas temporaryEditingEntity;
    /** A copy of the entity that we're currently selecting */
    private ITridas temporarySelectingEntity;

    private TridasEntityListHolder lists;

    private JPanel panelTopBar;
    private JPanel panelBottomBar;

    //// TOP PANEL ITEMS
    private JLabel topLabel;
    private EntityListComboBox cboTopChooser;
    private ChoiceComboBoxActionListener topChooserListener;
    private JButton btnSelectEntity;
    private JButton btnRevert;
    private JButton btnNewEntity;
    /** true if combo box is enabled for changing, false otherwise */
    private boolean changingTop;
    private static final String CHANGE_STATE = I18n.getText("general.change");
    private static final String OK_STATE = I18n.getText("general.choose");

    /** The lock/unlock button for making changes to the currently selected entity */
    private JToggleButton btnEditEntity;
    /** Text associated with lock/unlock button */
    private JLabel lblEditEntityText;
    /** The save button when unlocked */
    private JButton btnEditEntitySave;
    /** The cancel button when unlocked */
    private JButton btnEditEntityCancel;

    private ButtonGroup entityButtons;

    /**
     * Constructor: Wrap around the given sample
     * 
     * @param s
     */
    public TridasMetadataPanel(Sample s) {
        this.sample = s;

        // ensure it has a lab code
        if (!s.hasMeta(Metadata.LABCODE)) {
            LabCode labcode = new LabCode();

            // populate it at least with series code
            labcode.setSeriesCode(s.getSeries().getTitle());
            s.setMeta(Metadata.LABCODE, labcode);
        }

        // select this in button handling routine
        currentMode = null;
        temporaryEditingEntity = temporarySelectingEntity = null;

        lists = new TridasEntityListHolder();
        // later, when we're added to a panel, tell the list holder
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                lists.setParentWindow(SwingUtilities.windowForComponent(TridasMetadataPanel.this));
            }
        });

        initBars();
        initPropertiesPanel();
        // only show a basic metadata editor for things that aren't DIRECT
        initButtonPanel(s.getSampleType() != SampleType.DIRECT);

        setLayout(new BorderLayout());

        // mainPanel has top, properties, bottom
        JPanel mainPanel = new JPanel();
        mainPanel.setLayout(new BorderLayout());

        mainPanel.add(panelTopBar, BorderLayout.NORTH);
        mainPanel.add(propertiesPanel, BorderLayout.CENTER);
        mainPanel.add(panelBottomBar, BorderLayout.SOUTH);

        add(mainPanel, BorderLayout.CENTER);
        JScrollPane sp = new JScrollPane(buttonBar, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        sp.getViewport().setBackground(Color.WHITE);
        add(sp, BorderLayout.WEST);

        // set up initial state!
        // first, ensure we have a series
        // (this shouldn't happen?)
        if (s.getSeries() == null)
            throw new IllegalArgumentException("MetadataPanel creation for sample without a series?");

        // then, make sure we set up our initial button state to make sense
        for (EditType type : EditType.values()) {
            if (type.getEntity(s) == null) {
                disableBelowEnableAbove(type);
                break;
            }
        }

        // get notified when a property changes
        ((PropertySheetTableModel) propertiesTable.getModel()).addPropertyChangeListener(this);
    }

    /** Scale an icon down to 20x20 */
    private ImageIcon scaleIcon20x20(ImageIcon icon) {
        return new ImageIcon(icon.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH));
    }

    /**
     * Called to enable editing
     * Responsible for loading the duplicate copy into the editor
     * 
     * @param enabled
     */
    @SuppressWarnings("unchecked")
    protected void enableEditing(boolean enabled) {
        propertiesTable.setEditable(enabled);

        lblEditEntityText.setFont(lblEditEntityText.getFont().deriveFont(Font.BOLD));
        lblEditEntityText
                .setText(enabled ? I18n.getText("metadata.currentlyEditingThis") + " " + currentMode.getTitle()
                        : I18n.getText("metadata.clickLockToEdit") + " " + currentMode.getTitle());

        if (enabled) {
            ITridas currentEntity = currentMode.getEntity(sample);
            if (currentEntity instanceof ITridasSeries)
                temporaryEditingEntity = TridasCloner.cloneSeriesRefValues((ITridasSeries) currentEntity,
                        (Class<? extends ITridasSeries>) currentEntity.getClass());
            else
                temporaryEditingEntity = TridasCloner.clone(currentEntity, currentEntity.getClass());

            // user chose to edit without choosing 'new', so be nice and make a new one for them
            if (temporaryEditingEntity == null && cboTopChooser.getSelectedItem() == EntityListComboBox.NEW_ITEM) {
                temporaryEditingEntity = currentMode.newInstance(sample);
                populateNewEntity(currentMode, temporaryEditingEntity);
            }

            if (temporaryEditingEntity != null)
                propertiesPanel.readFromObject(temporaryEditingEntity);
            currentMode.clearChanged();
        } else {
            temporaryEditingEntity = null;

            // don't display anything if we have nothingk!
            ITridas entity = currentMode.getEntity(sample);
            if (entity != null)
                propertiesPanel.readFromObject(entity);
        }

        // disable choosing other stuff while we're editing
        // but only if something else doesn't disable it first
        if (cboTopChooser.isEnabled())
            cboTopChooser.setEnabled(!enabled);
        btnSelectEntity.setEnabled(!enabled);

        // show/hide our buttons
        btnEditEntitySave.setEnabled(true);
        btnEditEntityCancel.setEnabled(true);
        btnEditEntitySave.setVisible(enabled);
        btnEditEntityCancel.setVisible(enabled);
    }

    private void initBars() {
        panelTopBar = new JPanel();
        panelBottomBar = new JPanel();
        panelTopBar.setLayout(new BoxLayout(panelTopBar, BoxLayout.X_AXIS));
        panelBottomBar.setLayout(new BoxLayout(panelBottomBar, BoxLayout.X_AXIS));

        ////////// TOP BAR
        topLabel = new JLabel(I18n.getText("general.initializing"));
        cboTopChooser = new EntityListComboBox();
        btnSelectEntity = new JButton(CHANGE_STATE);
        btnRevert = new JButton(I18n.getText("general.revert"));
        btnNewEntity = new JButton("New");
        panelTopBar.add(topLabel);
        panelTopBar.add(Box.createHorizontalStrut(6));
        panelTopBar.add(cboTopChooser);
        panelTopBar.add(Box.createHorizontalStrut(6));
        panelTopBar.add(btnSelectEntity);
        panelTopBar.add(Box.createHorizontalStrut(6));
        panelTopBar.add(btnNewEntity);
        panelTopBar.add(Box.createHorizontalStrut(6));
        panelTopBar.add(btnRevert);
        panelTopBar.add(Box.createHorizontalGlue());
        panelTopBar.setBorder(BorderFactory.createEmptyBorder(2, 8, 2, 8));

        changingTop = false;
        btnRevert.setVisible(false);
        btnNewEntity.setVisible(false);

        btnSelectEntity.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (changingTop)
                    chooseButtonPressed();
                else
                    changeButtonPressed();

            }
        });

        btnRevert.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                changingTop = false;

                // revert back
                temporarySelectingEntity = null;
                // reselect the old thing (this calls topChooser's actionlistener, be careful!)
                selectInCombo(currentMode.getEntity(sample));

                btnEditEntity.setEnabled(!changingTop);
                cboTopChooser.setEnabled(changingTop);
                btnSelectEntity.setText(changingTop ? OK_STATE : CHANGE_STATE);
                btnRevert.setVisible(changingTop);
                btnNewEntity.setVisible(changingTop);
            }
        });

        btnNewEntity.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                selectInCombo(null);

                if (changingTop)
                    chooseButtonPressed();
                else
                    changeButtonPressed();
            }

        });

        topChooserListener = new ChoiceComboBoxActionListener(this);
        cboTopChooser.addActionListener(topChooserListener);

        ////////// BOTTOM BAR
        btnEditEntity = new JToggleButton();
        btnEditEntity.setIcon(scaleIcon20x20((ImageIcon) Builder.getIcon("lock.png", Builder.ICONS, 32)));
        btnEditEntity.setSelectedIcon(scaleIcon20x20((ImageIcon) Builder.getIcon("unlock.png", Builder.ICONS, 32)));
        btnEditEntity.setBorderPainted(false);
        btnEditEntity.setContentAreaFilled(false);
        btnEditEntity.setFocusable(false);

        btnEditEntity.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (!btnEditEntity.isSelected() && currentMode.hasChanged()) {
                    int response = saveBeforeChange();

                    if (response == JOptionPane.NO_OPTION) {
                        currentMode.clearChanged();
                    } else if (response == JOptionPane.CANCEL_OPTION) {
                        btnEditEntity.setSelected(true);
                        return;
                    } else {
                        doSave();
                    }
                }
                enableEditing(btnEditEntity.isSelected());
            }
        });

        panelBottomBar.add(btnEditEntity);

        lblEditEntityText = new JLabel(I18n.getText("general.initializing").toLowerCase());
        lblEditEntityText.setLabelFor(btnEditEntity);
        panelBottomBar.add(lblEditEntityText);

        btnEditEntitySave = new JButton(I18n.getText("general.saveChanges"));
        btnEditEntityCancel = new JButton(I18n.getText("general.cancel"));

        // don't let an errant enter key fire these buttons!
        btnEditEntitySave.setDefaultCapable(false);
        btnEditEntityCancel.setDefaultCapable(false);

        btnEditEntitySave.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                doSave();
            }
        });

        btnEditEntityCancel.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                currentMode.clearChanged();
                btnEditEntity.setSelected(false);
                enableEditing(false);
            }
        });

        panelBottomBar.add(Box.createHorizontalGlue());
        panelBottomBar.add(btnEditEntitySave);
        panelBottomBar.add(Box.createHorizontalStrut(6));
        panelBottomBar.add(btnEditEntityCancel);
        panelBottomBar.add(Box.createHorizontalStrut(6));
    }

    /**
     * Called by either button press to update the UI state
     */
    private void chooseOrCancelUIUpdate() {
        // disable/enable editing
        btnEditEntity.setEnabled(!changingTop);
        cboTopChooser.setEnabled(changingTop);
        btnSelectEntity.setText(changingTop ? OK_STATE : CHANGE_STATE);
        btnRevert.setVisible(changingTop);
        btnNewEntity.setVisible(changingTop);
    }

    /**
     * Called when the 'Change' button is pressed
     * Activates the combo box, allowing for changes
     */
    private void changeButtonPressed() {

        // Make sure we populate our combobox with everything from the server
        populateComboAndSelect(true);

        if (cboTopChooser.getSelectedItem() == EntityListComboBox.NEW_ITEM)
            propertiesTable.setWatermarkText(I18n.getText("general.choose").toUpperCase());
        else
            propertiesTable.setWatermarkText(I18n.getText("general.preview").toUpperCase());
        propertiesTable.setHasWatermark(true);

        // we're starting to change...
        changingTop = true;

        chooseOrCancelUIUpdate();
    }

    /**
     * Called when the 'Choose' button is pressed
     * Sets the current entity to what is present in the combo box
     */
    private void chooseButtonPressed() {
        boolean flaggedAsNew = false;

        // 
        // HACK:
        // Don't allow people to select 
        //
        if (temporarySelectingEntity != null && !(temporarySelectingEntity instanceof TridasObject)
                && !(temporarySelectingEntity.equals(currentMode.getEntity(sample)))
                && temporarySelectingEntity.getTitle().contains("(")) {

            JOptionPane.showMessageDialog(this, I18n.getText("error.legacyEntityProblem"), I18n.getText("error"),
                    JOptionPane.ERROR_MESSAGE);

            return;
        }
        //
        // END HACK
        //

        //
        // ANOTHER HACK!!
        //      
        // If it's a new object, then ask for a lab code
        /*String code = null;
        if(  (temporarySelectingEntity instanceof TridasObject) && 
            (cboTopChooser.getSelectedItem() == EntityListComboBox.NEW_ITEM))
        {
           // TODO replace this with an actual field in the metadata table
           code = JOptionPane.showInputDialog("Please provide a code for this object");   
           if(code!=null)
           {            
        TridasGenericField gf = new TridasGenericField();
        gf.setName(TridasUtils.GENERIC_FIELD_STRING_OBJECTCODE);
        gf.setValue(code);
        gf.setType("xs:string");
        ((TridasObject)temporarySelectingEntity).getGenericFields().add(gf);
           }
        }*/
        //
        // END HACK
        // 

        propertiesTable.setHasWatermark(false);
        propertiesTable.setWatermarkText(null);

        // the user is changing away...
        if (temporarySelectingEntity != null && !temporarySelectingEntity.equals(currentMode.getEntity(sample))) {
            currentMode.setEntity(sample, temporarySelectingEntity);
            temporarySelectingEntity = null;

            // user selected 'new'
            if (cboTopChooser.getSelectedItem() == EntityListComboBox.NEW_ITEM) {
                flaggedAsNew = true;
                disableBelowEnableAbove(currentMode);

                // remove any associated bits BELOW the 'new'
                for (EditType et = currentMode.next(); et != null; et = et.next())
                    et.setEntity(sample, null);
            }
            // user chose an existing item
            else {
                // do we have to do anything special for the next page?
                EditType next = currentMode.next();
                if (next != null) {
                    // disable buttons that we can't use
                    disableBelowEnableAbove(next);

                    // remove any associated bits from the sample
                    for (EditType et = next; et != null; et = et.next())
                        et.setEntity(sample, null);
                }

                currentMode.clearChanged();
            }

            // we modified the series...
            sample.setModified();
            // notify the sample that it changed
            sample.fireSampleMetadataChanged();
        }

        // we're done changing...
        changingTop = false;

        // update the UI...
        chooseOrCancelUIUpdate();

        // if it's new, start editing right away
        if (flaggedAsNew)
            btnEditEntity.doClick();
    }

    /**
     * Populate any parameters in a newly created entity
     * @param type the current EditType
     * @param entity the corresponding entity
     */
    protected void populateNewEntity(EditType type, ITridas entity) {

        entity.setTitle("New " + type.displayTitle);

        // Autopopulate TridasRadius features for ease of use
        if (entity instanceof TridasRadius) {
            TridasWoodCompleteness wc = new TridasWoodCompleteness();

            TridasPith pith = new TridasPith();
            pith.setPresence(ComplexPresenceAbsence.UNKNOWN);
            wc.setPith(pith);

            TridasHeartwood hw = new TridasHeartwood();
            hw.setPresence(ComplexPresenceAbsence.UNKNOWN);
            wc.setHeartwood(hw);

            TridasSapwood sw = new TridasSapwood();
            sw.setPresence(ComplexPresenceAbsence.UNKNOWN);
            TridasLastRingUnderBark lrub = new TridasLastRingUnderBark();
            lrub.setPresence(PresenceAbsence.UNKNOWN);
            sw.setLastRingUnderBark(lrub);
            wc.setSapwood(sw);

            TridasBark bark = new TridasBark();
            bark.setPresence(PresenceAbsence.UNKNOWN);
            wc.setBark(bark);

            ((TridasRadius) entity).setWoodCompleteness(wc);
        }

    }

    private void initPropertiesPanel() {
        propertiesTable = new TellervoPropertySheetTable();

        propertiesPanel = new TellervoPropertySheetPanel(propertiesTable);

        // keep property collapse/expand states
        propertiesPanel.setRestoreToggleStates(true);

        propertiesPanel.setToolBarVisible(false);
        propertiesPanel.setDescriptionVisible(true);

        propertiesPanel.setMode(PropertySheet.VIEW_AS_CATEGORIES);

        propertiesPanel.getTable().setRowHeight(24);
        propertiesPanel.getTable().setRendererFactory(new TridasPropertyRendererFactory());
        propertiesPanel.getTable().setEditorFactory(new TridasPropertyEditorFactory());
    }

    /**
     * @return true if the user wants to lose the new selection, false otherwise
     */
    private boolean warnLosingSelection() {

        // nothing new has been selected, nothing to lose
        if (temporarySelectingEntity == null || temporarySelectingEntity.equals(currentMode.getEntity(sample)))
            return true;

        int ret = JOptionPane.showConfirmDialog(this, I18n.getText("question.confirmChangeForm"),
                I18n.getText("question.continue"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);

        return (ret == JOptionPane.YES_OPTION);
    }

    /**
     * @return true if the user wants to lose changes, false otherwise
     */
    /*private boolean warnLosingChanges() {
       if(currentMode == null || !currentMode.hasChanged())
     return true;
           
       int ret = JOptionPane.showConfirmDialog(this, 
        I18n.getText("question.confirmChangeForm"), 
        I18n.getText("question.continue"), 
        JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
           
       return (ret == JOptionPane.YES_OPTION);
    }*/

    private int saveBeforeChange() {

        if (currentMode == null || !currentMode.hasChanged())
            // Nothing changed so no saving required
            return JOptionPane.NO_OPTION;

        Object[] options = { "Save", "Discard", "Cancel" };
        int ret = JOptionPane.showOptionDialog(this,
                "You have unsaved changes to the metadata for this entity.\n\nWould you like to save before continuing?",
                "Save changes?", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, options,
                options[0]);

        return ret;

    }

    private void doSave() {
        if (temporaryEditingEntity == null)
            throw new IllegalStateException();

        // if nothing actually changed, just ignore it like a cancel
        if (!currentMode.hasChanged()) {
            btnEditEntityCancel.doClick();
            return;
        }

        propertiesPanel.writeToObject(temporaryEditingEntity);

        // are we saving something new?
        boolean isNew = false;
        EditType prevMode = currentMode.previous();
        ITridas parentEntity = (prevMode == null) ? null : prevMode.getEntity(sample);

        // logic for saving...
        if (currentMode == EditType.DERIVED_SERIES || currentMode == EditType.MEASUREMENT_SERIES) {
            // nice and easy... save the series directly into the sample, that's it!
            currentMode.setEntity(sample, temporaryEditingEntity);
        } else {
            // ok, we're saving an entity. save to the server!

            // sanity check to ensure parent entity being null means we're an object
            if (parentEntity == null && currentMode != EditType.OBJECT) {
                new BugDialog(new IllegalStateException("parentEntity is null, but not an object"));
                return;
            }

            // is it new? (no identifier?)
            isNew = !temporaryEditingEntity.isSetIdentifier();

            // the resource we'll use
            EntityResource<? extends ITridas> resource;

            if (isNew)
                resource = getNewAccessorResource(temporaryEditingEntity, parentEntity, currentMode.getType());
            else
                resource = getUpdateAccessorResource(temporaryEditingEntity, currentMode.getType());

            // set up a dialog...
            Window parentWindow = SwingUtilities.getWindowAncestor(this);
            TellervoResourceAccessDialog dialog = TellervoResourceAccessDialog.forWindow(parentWindow, resource);

            // query the resource
            resource.query();
            dialog.setVisible(true);

            // on failure, just return
            if (!dialog.isSuccessful()) {
                JOptionPane.showMessageDialog(this,
                        I18n.getText("error.savingChanges") + System.lineSeparator()
                                + dialog.getFailException().getLocalizedMessage(),
                        I18n.getText("error"), JOptionPane.ERROR_MESSAGE);
                return;
            }

            // replace the saved result
            temporaryEditingEntity = resource.getAssociatedResult();

            // sanity check the result
            if (temporaryEditingEntity == null) {
                new BugDialog(new IllegalStateException("CREATE or UPDATE entity returned null"));
                return;
            }

            // take the return value and save it
            currentMode.setEntity(sample, temporaryEditingEntity);
        }

        // if it was new, re-enable our editing
        if (isNew) {
            // stick it in the combo box list
            if (parentEntity == null) {
                // it's an object...
                // TODO: Fix new objects?
            } else
                lists.appendChildToList(parentEntity, temporaryEditingEntity);

            // repopulate the combo box...
            populateComboAndSelect(false);

            EditType next = currentMode.next();
            if (next != null) {
                // only prepare a child list if we can have children... 
                lists.prepareChildList(temporaryEditingEntity);

                disableBelowEnableAbove(currentMode.next());
            }

            // also, add it to the valid combo box...
        }

        // on success...
        currentMode.clearChanged();
        btnEditEntity.setSelected(false);
        enableEditing(false);
        populateComboAndSelect(false);
        temporaryEditingEntity = null;

        sample.setModified();
        sample.fireSampleMetadataChanged();
    }

    /**
     * For creating a new entity on the server
     * 
     * @param <T>
     * @param entity
     * @param parent
     * @param type
     * @return
     */
    private <T extends ITridas> EntityResource<T> getNewAccessorResource(ITridas entity, ITridas parent,
            Class<T> type) {
        return new EntityResource<T>(entity, parent, type);
    }

    /**
     * For updating an existing entity on the server
     * 
     * @param <T>
     * @param entity
     * @param type
     * @return
     */
    private <T extends ITridas> EntityResource<T> getUpdateAccessorResource(ITridas entity, Class<T> type) {
        return new EntityResource<T>(entity, TellervoRequestType.UPDATE, type);
    }

    /**
     * Get an entity list for the current mode
     * @param goRemote should we try and load from the remote server?
     * @return
     */
    private List<? extends ITridas> getEntityList(boolean goRemote) {
        return getEntityList(currentMode, goRemote);
    }

    /**
     * Sort a list of ITridas objects
     * 
     * @param list
     */
    private void sortList(List<? extends ITridas> list) {
        // Sort list intelligently
        TridasComparator numSorter = new TridasComparator(TridasComparator.Type.TITLES,
                TridasComparator.NullBehavior.NULLS_LAST, TridasComparator.CompareBehavior.AS_NUMBERS_THEN_STRINGS);

        Collections.sort(list, numSorter);
    }

    /**
     * Get an entity list for the given mode
     * Returns a list to populate the combo box
     * @param mode the mode
     * @param goRemote should we try and load from the remote server?
     * @return
     */
    private List<? extends ITridas> getEntityList(EditType mode, boolean goRemote) {
        switch (mode) {

        /*case PROJECT:
           return App.tridasProjects.getProjectList();*/

        case OBJECT:
            return App.tridasObjects.getObjectList();

        case ELEMENT:
        case SAMPLE:
        case RADIUS: {
            List<ITridas> list;

            try {
                list = lists.getChildList(currentMode.previous().getEntity(sample), goRemote);
            } catch (Exception e) {
                new BugDialog(e);
                list = null;
            }

            // get what we already have selected
            ITridas singleton = mode.getEntity(sample);

            // stuff in the list? keep it
            if (list != null && !list.isEmpty()) {
                // make sure the currently selected entity is represented in the list
                if (singleton != null && entityInList(singleton, list) == null)
                    list.add(singleton);

                // sort it nicely
                sortList(list);

                return list;
            }

            // Ok, there's nothing 
            if (singleton != null)
                return Collections.singletonList(singleton);
            else
                return Collections.emptyList();
        }

        default:
            return Collections.emptyList();
        }
    }

    /**
     * Compare entities based on type, title, and site code (of they're TridasObjectEx)
     * @param e1
     * @param e2
     * @return
     */
    private boolean matchEntities(ITridas e1, ITridas e2) {
        // either are null? -> no match
        if (e1 == null || e2 == null)
            return false;

        // easy way out: identity!
        if (e1 == e2)
            return true;

        // they're not related classes -> not equal!
        if (!(e1.getClass().isAssignableFrom(e2.getClass()) || e2.getClass().isAssignableFrom(e1.getClass())))
            return false;

        // compare lab codes for TridasObjectExes
        if (e1 instanceof TridasObjectEx && e2 instanceof TridasObjectEx) {
            TridasObjectEx obj1 = (TridasObjectEx) e1;
            TridasObjectEx obj2 = (TridasObjectEx) e2;

            // not the same lab code -> not equal
            if (obj1.hasLabCode() && obj2.hasLabCode() && !obj1.getLabCode().equals(obj2.getLabCode()))
                return false;
        }

        // we found a match!
        if (e1.getTitle().equals(e2.getTitle()))
            return true;

        return false;
    }

    /**
     * Checks to see if the entity is in the given list
     * 
     * @param entity
     * @param list
     * @return The entity in the list (may be another instance) or null
     */
    private ITridas entityInList(ITridas entity, List<?> list) {
        for (Object o : list) {
            if (o instanceof ITridas) {
                ITridas otherEntity = (ITridas) o;

                if (matchEntities(entity, otherEntity))
                    return otherEntity;
            }
        }

        return null;
    }

    /**
     * Initialize the state when we:
     * a) choose something new in the combo box (load children!)
     * b) programmatically set something in the combo box (don't load children!)
     */
    private void handleComboSelection(boolean loadChildren) {
        Object obj = cboTopChooser.getSelectedItem();

        //fixes bug where combobox is hidden in derived series
        if (obj == null) {
            return;
        }

        // if it's new, create a new, empty instance
        if (obj == EntityListComboBox.NEW_ITEM) {
            temporarySelectingEntity = currentMode.newInstance(sample);
            populateNewEntity(currentMode, temporarySelectingEntity);

            if (propertiesTable.hasWatermark())
                propertiesTable.setWatermarkText("NEW...");
        } else if (obj instanceof ITridas) {
            temporarySelectingEntity = (ITridas) obj;

            //
            // HACK
            // Warn the user about legacy cruft
            //
            if (temporarySelectingEntity.getTitle().contains("(")
                    && !(temporarySelectingEntity instanceof TridasObject)) {
                if (propertiesTable.hasWatermark())
                    propertiesTable.setWatermarkText(I18n.getText("general.legacy"));
            } else {
                // this part isn't a hack...
                if (propertiesTable.hasWatermark())
                    propertiesTable.setWatermarkText(I18n.getText("general.preview"));
            }
            //
            // END HACK
            //

            // start loading the list of children right away
            if (loadChildren && currentMode != EditType.RADIUS)
                lists.prepareChildList(temporarySelectingEntity);
        }

        propertiesPanel.readFromObject(temporarySelectingEntity);
        currentMode.clearChanged();
    }

    /**
     * Select an item in the combo box
     * Use because TridasEntity.equals compares things we don't care about
     * 
     * @param entity
     */
    private void selectInCombo(ITridas entity) {
        // disable actionListener firing when we change combobox selection
        topChooserListener.setEnabled(false);
        btnNewEntity.setVisible(false);

        try {
            if (entity == null) {
                cboTopChooser.setSelectedItem(EntityListComboBox.NEW_ITEM);
                return;
            }

            FilterableComboBoxModel model = (FilterableComboBoxModel) cboTopChooser.getModel();

            // find it in the list...
            ITridas listEntity;
            if ((listEntity = entityInList(entity, model.getElements())) != null) {
                cboTopChooser.setSelectedItem(listEntity);
                return;
            }

            // blech, it wasn't in the list -> add it
            model.insertElementAt(entity, 2);
            cboTopChooser.setSelectedItem(entity);
        } finally {
            // deal with the selection
            handleComboSelection(false);
            // re-enable combo box actionlistener firing
            topChooserListener.setEnabled(true);
            btnNewEntity.setVisible(true);

        }
    }

    /**
     * Populate the combo box with a list
     * @param goRemote ensure we have remote stuff in the list
     */
    private void populateComboAndSelect(boolean goRemote) {
        // get the list of stuff that goes in the box
        List<? extends ITridas> entityList = getEntityList(goRemote);

        cboTopChooser.setList(entityList);

        // select what we already have, if it exists
        ITridas selectedEntity = currentMode.getEntity(sample);

        // otherwise, try to choose something nicely
        if (selectedEntity == null)
            selectedEntity = suggestSelectedEntity(currentMode, entityList);

        selectInCombo(selectedEntity);
    }

    /**
     * Suggest a selection for the given mode
     * 
     * @param mode the mode
     * @param list a list of acceptable options for the mode
     * @return null to choose 'new', a member of the list, or really any ITridas derived thing
     */
    protected ITridas suggestSelectedEntity(EditType mode, List<? extends ITridas> list) {
        if (list.size() == 1)
            return list.get(0);

        return null;
    }

    /**
     * Called whenever a button on the button bar is pushed
     * 
     * @param type
     */
    private void buttonAction(EditType type) {
        // ignore duplicate clicks
        if (currentMode == type)
            return;

        // don't show two dialogs...
        boolean alreadyWarned = false;

        if (currentMode != null) {
            // prompt if the user wants to save any changes they made
            if (currentMode.hasChanged()) {

                int response = saveBeforeChange();

                if (response == JOptionPane.CANCEL_OPTION) {
                    currentMode.getButton().doClick();
                    return;
                } else if (response == JOptionPane.NO_OPTION) {

                } else {
                    doSave();
                }

                currentMode.clearChanged();

                alreadyWarned = true;
            }

            if (currentMode.isBrandNew(sample)
                    && !(currentMode == EditType.MEASUREMENT_SERIES || currentMode == EditType.DERIVED_SERIES)) {
                // if it's brand new and the user has indicated they don't
                // care about the changes, destroy it; it's useless!
                // BUT only do this if it's a mid-level entity (don't touch samples!)
                currentMode.setEntity(sample, null);
            }

        }

        // cancel out of changing (eg object) if the user decides to..
        if (changingTop) {
            if (!alreadyWarned && !warnLosingSelection()) {
                // act like the user clicked the old button again
                currentMode.getButton().doClick();
                return;
            }

            btnRevert.doClick();
        }

        // clear off any overlay text we might have
        propertiesTable.setHasWatermark(false);
        propertiesTable.setWatermarkText(null);

        // load the new current entity
        currentMode = type;

        // derive a property list
        List<TridasEntityProperty> properties = TridasEntityDeriver.buildDerivationList(type.getType());
        Property[] propArray = properties.toArray(new Property[properties.size()]);

        // set properties and load from entity
        propertiesPanel.setProperties(propArray);

        // handle top bar
        if (currentMode == EditType.DERIVED_SERIES || currentMode == EditType.MEASUREMENT_SERIES) {
            cboTopChooser.setVisible(false);
            btnSelectEntity.setVisible(false);
            topLabel.setText(I18n.getText("metadata.for") + " " + currentMode.getTitle() + " "
                    + sample.getMetaString(Metadata.TITLE));
            topLabel.setLabelFor(null);
        } else {
            // metadata for a remote object
            cboTopChooser.setVisible(true);

            String prefix;
            LabCode labcode = sample.getMeta(Metadata.LABCODE, LabCode.class);

            switch (currentMode) {
            case ELEMENT:
                prefix = " " + LabCodeFormatter.getElementPrefixFormatter().format(labcode) + "-";
                break;

            case SAMPLE:
                prefix = " " + LabCodeFormatter.getSamplePrefixFormatter().format(labcode) + "-";
                break;

            case RADIUS:
                prefix = " " + LabCodeFormatter.getRadiusPrefixFormatter().format(labcode) + "-";
                break;

            // nothing for object..
            default:
                prefix = "";
                break;
            }

            topLabel.setText(I18n.getText("metadata.for") + " " + currentMode.getTitle() + prefix);
            topLabel.setLabelFor(cboTopChooser);

            btnSelectEntity.setVisible(true);
            btnSelectEntity.setEnabled(true);
            cboTopChooser.setEnabled(false);

            populateComboAndSelect(false);

            // if don't have this entity, enable the list by default
            if (currentMode.getEntity(sample) == null)
                btnSelectEntity.doClick();
        }

        // by default, disable editing except on series
        // note that editEntity loads the properties into the panel for us
        switch (type) {
        case MEASUREMENT_SERIES:
        case DERIVED_SERIES:
            enableEditing(true);
            btnEditEntity.setSelected(true);
            break;

        default:
            enableEditing(false);
            btnEditEntity.setSelected(false);
            break;
        }

        currentMode.clearChanged();
    }

    /**
     * Disable buttons below, enable buttons above
     * @param type
     */
    private void disableBelowEnableAbove(EditType type) {
        boolean enabled = true;

        for (EditType t : EditType.values()) {
            t.enableAssociatedButton(enabled);

            if (t == type)
                enabled = false;
        }
    }

    /**
     * Creates a button to the button panel
     * @param type
     * @return
     */
    private AbstractButton createButton(final EditType type) {
        Action action = new AbstractAction(type.getTitle(), type.getIcon()) {
            public void actionPerformed(ActionEvent e) {
                buttonAction(type);
            }
        };

        JToggleButton button = new JToggleButton(action);
        type.associateButton(button);

        return button;
    }

    /**
     * Set up the button panel
     * @param isDerived
     */
    private void initButtonPanel(boolean isDerived) {
        buttonBar = new ScrollableJButtonBar(JButtonBar.VERTICAL);
        //buttonBar = new JButtonBar(JButtonBar.VERTICAL);

        buttonBar.setUI(new BlueishButtonBarUI());

        AbstractButton button;
        entityButtons = new ButtonGroup();

        // special case for derived series
        if (isDerived) {
            button = createButton(EditType.DERIVED_SERIES);
            entityButtons.add(button);
            buttonBar.add(button);
            button.doClick();
        } else {
            for (EditType t : EditType.values()) {
                // skip over these series types if we're not dealing with them
                if (t == EditType.DERIVED_SERIES)
                    continue;
                /*if(t == EditType.PROJECT)
                   continue;*/

                button = createButton(t);
                entityButtons.add(button);
                buttonBar.add(button);

                // start on measurement series
                if (t == EditType.MEASUREMENT_SERIES)
                    button.doClick();
            }
        }

        buttonBar.setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4));

    }

    /**
     * Convenient way to handle various types
     * 
     * @author Lucas Madar
     */

    public static enum EditType {
        //PROJECT(TridasProject.class, "Project", "project.png", Metadata.PROJECT),
        OBJECT(TridasObjectEx.class, I18n.getText("tridas.object"), "object.png", Metadata.OBJECT), ELEMENT(
                TridasElement.class, I18n.getText("tridas.element"), "element.png",
                Metadata.ELEMENT), SAMPLE(TridasSample.class, I18n.getText("tridas.sample"), "sample.png",
                        Metadata.SAMPLE), RADIUS(TridasRadius.class, I18n.getText("tridas.radius"), "radius.png",
                                Metadata.RADIUS), MEASUREMENT_SERIES(TridasMeasurementSeries.class,
                                        I18n.getText("tridas.measurementSeries"), "measurementseries.png",
                                        null), DERIVED_SERIES(TridasDerivedSeries.class,
                                                I18n.getText("tridas.derivedSeries"), "derivedseries.png", null);
        //BOX(WSIBox.class, I18n.getText("general.box"), "box.png", Metadata.BOX);

        private Class<? extends ITridas> type;
        private String displayTitle;
        private String iconPath;
        private String metadataTag;

        /** The JToggleButton associated with this edit state in the button pane */
        private AbstractButton associatedButton;
        /** Has this been edited? */
        private boolean hasChanged;

        private EditType(Class<? extends ITridas> type, String displayTitle, String iconPath, String metadataTag) {
            this.type = type;
            this.displayTitle = displayTitle;
            this.iconPath = iconPath;
            this.metadataTag = metadataTag;

            associatedButton = null;
        }

        public Class<? extends ITridas> getType() {
            return type;
        }

        public String getTitle() {
            return displayTitle;
        }

        public Icon getIcon() {
            return Builder.getIcon(iconPath, Builder.ICONS, 48);
        }

        /** 
         * Get an entity from the sample
         * 
         * @param s
         * @return
         */
        public ITridas getEntity(Sample s) {
            if (this == DERIVED_SERIES || this == MEASUREMENT_SERIES)
                return s.getSeries();

            return s.getMeta(metadataTag, ITridas.class);
        }

        /**
         * Sets the entity in the appropriate place in this sample
         * 
         * @param s
         * @param entity
         */
        public void setEntity(Sample s, ITridas entity) {
            // labcode, for updating
            // it must have been set in constructor, so no error check
            LabCode labcode = s.getMeta(Metadata.LABCODE, LabCode.class);

            if (this == DERIVED_SERIES || this == MEASUREMENT_SERIES) {
                s.setSeries((ITridasSeries) entity);

                // update series code
                labcode.setSeriesCode((entity == null) ? null : entity.getTitle());
            } else {

                // special case for objects: have to rebuild object array
                if (this == OBJECT) {
                    TridasObject[] objects = null;

                    if (entity instanceof TridasObjectEx) {
                        TridasObjectEx object = (TridasObjectEx) entity;
                        List<TridasObjectEx> objList = new ArrayList<TridasObjectEx>();

                        // add all objects to list from bottom->top
                        while (object != null) {
                            objList.add(object);
                            object = object.getParent();
                        }

                        // reverse, so list is top->bottom
                        Collections.reverse(objList);

                        // list -> array
                        objects = objList.toArray(new TridasObjectEx[0]);
                    } else if (entity != null)
                        objects = new TridasObject[] { (TridasObject) entity };

                    s.setMeta(Metadata.OBJECT_ARRAY, objects);
                }

                // set the metadata tag
                s.setMeta(metadataTag, entity);

                switch (this) {
                case OBJECT:
                    // site
                    labcode.clearSites();
                    if (s.hasMeta(Metadata.OBJECT_ARRAY) && s.getMeta(Metadata.OBJECT_ARRAY) != null) {
                        for (TridasObject obj : s.getMeta(Metadata.OBJECT_ARRAY, TridasObject[].class)) {
                            if (obj instanceof TridasObjectEx) {
                                labcode.appendSiteCode(((TridasObjectEx) obj).getLabCode());
                                labcode.appendSiteTitle(obj.getTitle());
                            } else {
                                labcode.appendSiteCode(obj.getTitle());
                                labcode.appendSiteTitle(obj.getTitle());
                            }
                        }
                    }
                    break;

                case ELEMENT:
                    labcode.setElementCode((entity == null) ? null : entity.getTitle());
                    break;

                case SAMPLE:
                    labcode.setSampleCode((entity == null) ? null : entity.getTitle());
                    break;

                case RADIUS:
                    labcode.setRadiusCode((entity == null) ? null : entity.getTitle());
                    break;
                default:
                    break;

                //case BOX:
                // ignore this...
                //return;
                }
            }

            s.setMeta(Metadata.TITLE, LabCodeFormatter.getDefaultFormatter().format(labcode));
        }

        /**
         * Associate a button with this type
         * @param button
         */
        public void associateButton(AbstractButton button) {
            associatedButton = button;
        }

        /**
         * Get the associated button
         * @return
         */
        public AbstractButton getButton() {
            return associatedButton;
        }

        /**
         * Enable or disable the associated button
         * Has no effect on series
         * @param enabled
         */
        public void enableAssociatedButton(boolean enabled) {
            //if(this == DERIVED_SERIES || this == MEASUREMENT_SERIES || this == BOX)
            if (this == DERIVED_SERIES || this == MEASUREMENT_SERIES)
                return;

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

        /**
         * Called when our underlying property changes
         */
        public void propertyChanged() {
            hasChanged = true;
        }

        public void clearChanged() {
            hasChanged = false;
        }

        public boolean hasChanged() {
            return hasChanged;
        }

        /**
         * @param s
         * @return true if the entity for this mode has no identifier
         */
        public boolean isBrandNew(Sample s) {
            return !(getEntity(s) != null && getEntity(s).isSetIdentifier());
        }

        public ITridas newInstance(Sample s) {
            try {
                return type.newInstance();
            } catch (Exception e) {
                new BugDialog(e);
                return null;
            }
        }

        /**
         * Gets the previous EditType in the order
         * @return
         */
        public EditType previous() {
            switch (this) {
            case ELEMENT:
                return OBJECT;
            case SAMPLE:
                return ELEMENT;
            case RADIUS:
                return SAMPLE;
            case MEASUREMENT_SERIES:
                return RADIUS;
            default:
                return null;
            }
        }

        /**
         * Gets the next EditType in the order
         * @return
         */
        public EditType next() {
            switch (this) {
            /*case PROJECT:
               return OBJECT;*/
            case OBJECT:
                return ELEMENT;
            case ELEMENT:
                return SAMPLE;
            case SAMPLE:
                return RADIUS;
            default:
                return null;
            }
        }
    }

    /**
     * Silly actionListener class that we can add/remove when we want to programmatically change the combo box
     * without triggering side effects
     */
    private class ChoiceComboBoxActionListener implements ActionListener {
        private final TridasMetadataPanel panel;
        private boolean enabled;

        public ChoiceComboBoxActionListener(TridasMetadataPanel panel) {
            this.panel = panel;
            this.enabled = true;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }

        public void actionPerformed(ActionEvent e) {
            if (enabled)
                panel.handleComboSelection(true);
        }
    }

    /**
     * Called when someone changes something on our property list
     */
    public void propertyChange(PropertyChangeEvent evt) {
        Object n;
        if (evt.getOldValue() == null && (n = evt.getNewValue()) != null && n.toString().equals(""))
            return;

        if (currentMode == null)
            throw new IllegalStateException("Property changed with null mode??");

        /*if(currentMode.getType().equals(TridasObjectEx.class))
           return;*/

        currentMode.propertyChanged();
    }

    public void showPage(EditType type) {

        for (Enumeration<AbstractButton> e = entityButtons.getElements(); e.hasMoreElements();) {
            JToggleButton btn = (JToggleButton) e.nextElement();
            if (btn.getText().equals(type.getTitle())) {
                btn.setSelected(true);
                buttonAction(type);
                return;
            }
        }

    }
}