/*
 * @(#)ArrayPanel.java    2.4.0   17 August 2005
 *
 * Copyright 2005
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.gui;

import edu.neu.ccs.*;
import edu.neu.ccs.codec.*;
import edu.neu.ccs.filter.*;
import edu.neu.ccs.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * <p>Class <code>ArrayPanel</code> is an abstract class that
 * is designed for the input of an array of a single type of 
 * <code>Stringable</code> object; this class must implement
 * the <code>TypedView</code> interface in such a way that
 * the data type associated with this view is constructed via
 * the <code>Stringable</code> array in some fashion.</p>
 *
 * <p>To implement <code>TypedView</code>, a derived class
 * must implement at least the following 3 methods:</p>
 *
 * <pre>    public abstract Class getDataType()</pre>
 *
 * <pre>    public abstract Stringable demandObject()</pre>
 *
 * <pre>    public abstract Stringable requestObject()</pre>
 * <pre>         throws CancelledException</pre>
 *
 * <p>An <code>ArrayPanel</code> is an integrated component 
 * that contains a collection of <code>TypedView</code>s for
 * input of the corresponding set of <code>Stringable</code>
 * objects.  The panel also provides for optional controls
 * that permit the end user to dynamically modify the length 
 * of the <code>TypedView</code> array.</p>
 *
 * <p>The developer can control the <code>TypedView</code> that
 * is inserted into the array panel by overriding the protected
 * method:</p>
 *
 * <pre>    protected TypedView createViewFor(int index)</pre>
 *
 * <p>The default implementation of this method returns a
 * generic <code>TextFieldView</code>.  It is very likely that
 * the developer will want to override this default definition.</p>
 *
 * <p>Experience with <code>ArrayPanel</code> has shown that
 * although the class is powerful it is also complex to use.
 * As of 2.4.0, we have introduced the <code>SimpleArrayPanel</code>
 * class that takes a different approach and is significantly
 * easier to use.  The class <code>SimpleArrayPanel</code> is
 * not abstract so it may be used directly without the need to
 * make a derived class that implements <code>TypedView</code>.</p>
 *
 * <p>For backward compatibility, the class <code>ArrayPanel</code>
 * has been left basically as is.</p>
 *
 * @author  Richard Rasala
 * @author  Jeff Raab
 * @version 2.4.0
 * @since   1.0
 * @see TypedView
 */
public abstract class ArrayPanel 
    extends DisplayPanel 
    implements TypedView 
{
    ////////////////////
    // Property names //
    ////////////////////

    /** Bound property name for the visible controls property. */
    public static final String CONTROLS = "controls";

    /** Bound property name for the increment button text. */
    public static final String INCREMENT_BUTTON_TEXT = 
        "increment.button.text";

    /** Bound property name for the decrement button text. */
    public static final String DECREMENT_BUTTON_TEXT = 
        "decrement.button.text";

    /** Bound property name for the length prompt. */
    public static final String LENGTH_PROMPT = 
        "length.prompt";

    /** Bound property name for the set button text. */
    public static final String SET_BUTTON_TEXT = 
        "set.button.text";

    /** Bound property name for the length of the array. */
    public static final String LENGTH = "length";

    /** Bound property name for the minimum length of the array. */
    public static final String MINIMUM_LENGTH = 
        "minimum.length";

    /** Bound property name for the maximum length of the array. */
    public static final String MAXIMUM_LENGTH = 
        "maximum.length";

    ///////////////
    // Constants //
    ///////////////

    /** Value designating no desired length controls. */
    public static final int NO_CONTROLS = 0;

    /** Value designating the increment and decrement controls. */
    public static final int INCREMENT_DECREMENT = 1;
    
    /** 
     * Value designating the length field 
     * and set button controls. 
     */
    public static final int LENGTH_TEXT_FIELD = 2;
    
    /** Bit mask for the valid controls options. */
    protected static final int CONTROLS_MASK = 
        INCREMENT_DECREMENT + LENGTH_TEXT_FIELD;

    /** Default value specifying the visible length controls. */
    public static final int DEFAULT_CONTROLS = 
        INCREMENT_DECREMENT + LENGTH_TEXT_FIELD;

    /** 
     * Default alignment of length controls
     * relative to the view collection. 
     */
    public static final int DEFAULT_ALIGNMENT = BELOW;

    /** Default direction of the view collection. */
    public static final int DEFAULT_ORIENTATION = VERTICAL;

    /** Default minimum length for the array. */
    public static final int DEFAULT_MINIMUM_LENGTH = 0;

    /** Default maximum length for the array. */
    public static final int DEFAULT_MAXIMUM_LENGTH = 
        Integer.MAX_VALUE;

    //////////////////////
    // Instance members //
    //////////////////////

    /** 
     * Alignment of the length controls 
     * relative to the view collection. 
     */
    protected int align = DEFAULT_ALIGNMENT;

    /** Value specifying the visible length controls. */
    protected int controlsValue = DEFAULT_CONTROLS;

    /** Minimum length of this array. */
    protected int minLength = DEFAULT_MINIMUM_LENGTH;

    /** Maximum length of this array. */
    protected int maxLength = DEFAULT_MAXIMUM_LENGTH;

    /** Minimum length of this array in user coordinates. */
    protected int userMinLength = 0;

    /** Maximum length of this array in user coordinates. */
    protected int userMaxLength = 0;

    /** The view collection used by this array. */
    protected DisplayCollection views = null;

    /** The view collection scrolling display. */
    protected ScrollableDisplay scroller = null;

    /** Panel containing the length controls. */
    protected DisplayPanel controlPanel = null;

    /** Action for incrementing the length of this array. */
    protected Action increment = null;

    /** Action for decrementing the length of this array. */
    protected Action decrement = null;
    
    /** 
     * Actions panel containing the increment and decrement 
     * array length actions. 
     */
    protected ActionsPanel lengthActions = new ActionsPanel();

    /** Text field for input of the array length. */
    protected TextFieldView lengthField = new TextFieldView("");
    
    /** 
     * Filter used by array length field to enforce
     * minimum and maximum array length.
     */
    protected RangeFilter.Long lengthFilter = null;

    /** 
     * Button for setting the length of this array 
     * to the value held in the field 
     * contained in the length controls. 
     */
    protected Action set = null;

    /** Actions panel containing the set array length action. */
    protected ActionsPanel setActions = new ActionsPanel();

    /** Display containing the length field and its prompt. */
    protected Display lengthControls = null;

    /** Property list for this view object. */
    protected InputProperties properties = new InputProperties();
    
    /** Default view state for the elements of this array. */
    protected String defaultViewState = null;

    //////////////////
    // Constructors //
    //////////////////
    
    /**
     * Constructs an array panel of length 0
     * with the default orientation, controls, and alignment.
     *
     * @see #ArrayPanel(Stringable[])
     * @see #ArrayPanel(Stringable[], int)
     * @see #ArrayPanel(Stringable[], int, int, int)
     */
    public ArrayPanel() {
        this(null,
             DEFAULT_ORIENTATION, 
             DEFAULT_CONTROLS, 
             DEFAULT_ALIGNMENT);
    }
    
    /**
     * Constructs an array panel 
     * displaying the given array of model objects 
     * with the default direction, controls, and alignment.
     *
     * If the given array is <code>null</code>,
     * the array is set to length 0.
     *
     * @param obj an array of data models 
     *      for the elements of the collection
     * @see #ArrayPanel()
     * @see #ArrayPanel(Stringable[], int)
     * @see #ArrayPanel(Stringable[], int, int, int)
     */
    public ArrayPanel(Stringable[] obj) {
        this(obj,
             DEFAULT_ORIENTATION, 
             DEFAULT_CONTROLS, 
             DEFAULT_ALIGNMENT);
    }

    /**
     * Constructs an array panel 
     * displaying the given array of model objects 
     * with the given orientation, 
     * and the default controls and alignment.
     *
     * If the given array is <code>null</code>,
     * the array is set to length 0.
     *
     * If the given orientation is not valid,
     * the default orientation is used.
     *
     * @param obj an array of data models 
     *      for the elements of the collection
     * @param orientation the orientation for the view collection 
     * @see #ArrayPanel()
     * @see #ArrayPanel(Stringable[])
     * @see #ArrayPanel(Stringable[], int, int, int)
     */
    public ArrayPanel(Stringable[] obj, int orientation) {
        this(obj,
             orientation,
             DEFAULT_CONTROLS,
             DEFAULT_ALIGNMENT);
    }

    /**
     * Constructs an array panel 
     * displaying the given array of model objects 
     * with the given orientation, controls and alignment.
     *
     * If the given array is <code>null</code>,
     * the array is set to length 0.
     *
     * If the given orientation is not valid,
     * the default orientation is used.
     *
     * If the given controls value is not valid,
     * the default controls are included.
     *
     * If the given alignment value is not valid,
     * the default alignment is used.
     *
     * @param obj an array of data models 
     *      for the elements of the collection
     * @param orientation the orientation for the view collection 
     * @param controls the length controls for the component
     * @param alignment the alignment of length controls 
     *      relative to the view collection
     * @see #ArrayPanel()
     * @see #ArrayPanel(Stringable[])
     * @see #ArrayPanel(Stringable[], int)
     */
    public ArrayPanel(
        Stringable[] obj, 
        int orientation, 
        int controls, 
        int alignment) 
    {
        // initialize main panel display
        setLayout(new BorderLayout());
        views = new DisplayCollection();
        scroller = new ScrollableDisplay(views);
        add(scroller, BorderLayout.CENTER);
        
        // initialize length filter variables
        userMinLength = transformRawLengthToUserLength(minLength);
        userMaxLength = transformRawLengthToUserLength(maxLength);
        
        lengthFilter =
            new RangeFilter.Long(userMinLength, userMaxLength) {
                public Stringable filterStringable(Stringable obj) 
                    throws FilterException 
                {
                    try {
                        return super.filterStringable(obj);
                    }
                    catch (FilterException ex) {
                        throw new FilterException(
                            obj, createLengthSetOutOfBoundsMessage(ex));
                    }
                }
            };
        
        // initialize length field display
        lengthField.setPreferredWidth(40);
        lengthControls = new Display(lengthField, "Length:", null);

        // initialize the direction, controls, alignment
        setOrientation(orientation);
        setControls(controls);
        setAlignment(alignment);
        
        // establish actions for array length
        increment = new SimpleAction("Increment") {
            public void perform() {
                setLength(getLength() + 1);
            }
        };

        decrement = new SimpleAction("Decrement") {
            public void perform() {
                setLength(getLength() - 1);
            }
        };
        
        lengthActions.addAction(increment);
        lengthActions.addAction(decrement);

        set = new SimpleAction("Set") {
            public void perform() {
                applyLengthFromControls();
            }
        };
        
        setActions.addAction(set);
        lengthField.addActionListener(set);

        // initialize the views for the elements
        // and set the initial length to match what is shown
        if (obj != null) {
            for (int i = 0; i < obj.length; i++)
                increment(obj[i].toStringData());
            
            setLength(obj.length);
        }
        else
            setLength(0);
    }

    ///////////////
    // TypedView //
    ///////////////
    
    public abstract Class getDataType();

    public abstract Stringable demandObject();
    
    public abstract Stringable requestObject() 
        throws CancelledException;

    public void setInputProperties(InputProperties p) {
        InputProperties oldProperties = properties;

        properties = p;
        
        // if the input properties has changed
        if ((getInputProperties() != null) &&
            !getInputProperties().equals(oldProperties)) 
        {

            // notify listeners of property change        
            firePropertyChange(
                INPUT_PROPERTIES,
                oldProperties,
                properties);
        }
    }

    public InputProperties getInputProperties() {
        return properties;
    }

    /////////////////
    // Displayable //
    /////////////////

    /**
     * <p>Sets the view states for 
     * <code>Displayable</code> objects in this collection 
     * to the data encoded in the given <code>String</code>.
     *
     * The length of this array is set to the number of view states
     * contained in the given encoded <code>String</code>
     * before the view states for the 
     * views in this collecion are set.
     *
     * Each object in the collection has its view state
     * set from the encoded <code>String</code> data
     * in the manner described in the API documentation for the
     * <code>{@link DisplayPanel DisplayPanel}</code> class.</p>
     *
     * <p>If the given encoded data is <code>null</code>,
     * the view state is not changed.
     *
     * If the given data is an encoding for <code>null</code>,
     * the view state is not changed.</p>
     *
     * <p>Implementation change: As of 2.4.0 decodes the data
     * parameter using <code>Strings.decode</code>.</p>
     * 
     * @param data the encoded <code>String</code> data
     * @see #getViewState()
     * @see Displayable
     */
    public void setViewState(String data) {
        if (data == null)
            return;
    
        // decode the data and bail if null
        String[] decoded = Strings.decode(data);
        if (decoded == null)
            return;
    
        // set the length of the array appropriately
        setLength(decoded.length);

        // use the DisplayPanel setViewState method
        views.setViewState(data);

        // notify listeners of property change        
        firePropertyChange(
            VIEW_STATE,
            null,
            data);
    }

    /**
     * Returns the view states 
     * for objects in the collection 
     * as an encoded <code>String</code>.
     *
     * The view state for the collection is encoded
     * in the manner described in the API documentation for the
     * <code>{@link DisplayPanel DisplayPanel}</code> class.
     *
     * @see #setViewState(String)
     * @see Displayable
     */
    public String getViewState() {
        return views.getViewState();
    }
    
    /**
     * Sets the default view state for the elements
     * of this array panel to the data 
     * encoded in the given <code>String</code>.
     *
     * If <code>null</code>, the default view state
     * for the collection is encoded in the manner described 
     * in the API documentation for the
     * <code>{@link DisplayPanel DisplayPanel}</code> class.
     *
     * @param data the desired default <code>String</code> data
     * @see #reset()
     * @see Displayable
     */
    public void setDefaultViewState(String data) {
        defaultViewState = data;

        // notify listeners of property change        
        firePropertyChange(
            DEFAULT_VIEW_STATE,
            null,
            data);
    }
    
    /**
     * Returns the default view state for this array panel
     * as an encoded <code>String</code>.
     *
     * If the default view state for this array panel
     * has been set to <code>null</code>, or has not been set,
     * the default view state for the collection is encoded
     * in the manner described in the API documentation for the
     * <code>{@link DisplayPanel DisplayPanel}</code> class.
     *
     * @see #setDefaultViewState(String)
     * @see #reset()
     */
    public String getDefaultViewState() {
        if (defaultViewState == null)
            return views.getDefaultViewState();
        
        return defaultViewState;
    }
    
    /**
     * Sets the view state for this array panel
     * to the default view state for this array panel.
     *
     * @see #setDefaultViewState(String)
     * @see Displayable
     */
    public void reset() {
        setViewState(getDefaultViewState());
    }

    ////////////////
    // Public API //
    ////////////////

    /**
     * Returns an array of model objects of the appropriate type
     * initialized from the view states 
     * for the elements of this array panel, 
     * using the mandatory model of IO.  
     *
     * This method relies on the individual element
     * views to provide their own error strategy.
     *
     * @see #requestObjectArray()
     * @see #demandObject()
     */
    public Stringable[] demandObjectArray() {
        
        // return a null array if appropriate
        if (getLength() == 0)
            return null;
            
        // otherwise compile the array of objects and return it
        Stringable[] obj = new Stringable[getLength()];
        for (int i = 0; i < getLength(); i++) {
            Display d = (Display)views.getItem(i);
            TypedView t = (TypedView)d.getDisplay();            
            obj[i] = t.demandObject();
        }
        
        return obj;
    }

    /**
     * Returns an array of model objects of the appropriate type
     * initialized from the view states
     * for the elements of this array panel, 
     * using the optional model of IO.  
     *
     * This method relies on the individual element
     * views to provide their own error strategy.
     *
     * @see #demandObjectArray()
     * @see #requestObject()
     * @throws CancelledException 
     *      if the user cancels the input operation
     */
    public Stringable[] requestObjectArray() 
        throws CancelledException 
    {
        // return a null array if appropriate
        if (getLength() == 0)
            return null;
            
        // otherwise compile the array of objects and return it
        Stringable[] obj = new Stringable[getLength()];
        for (int i = 0; i < getLength(); i++) {
            Display d = (Display)views.getItem(i);
            TypedView t = (TypedView)d.getDisplay();            
            obj[i] = t.requestObject();
        }
        
        return obj;
    }

    /**
     * Returns the view element at the given index.
     *
     * @param index the index of the view to be returned
     */
    public TypedView getView(int index) {
        Component c = views.getItem(index);
        Display d = (Display)c;
        return (TypedView)d.getDisplay();
    }

    /**
     * Sets the orientation of this view collection 
     * to the given orientation value.
     *
     * If the orientation value is not valid,
     * the current orientation is not changed.
     *
     * @param orientation the new orientation for this collection
     * @see #getOrientation()
     * @see #HORIZONTAL
     * @see #VERTICAL
     * @see #DEFAULT
     */
    public void setOrientation(int orientation) {
        int oldOrientation = getOrientation();

        views.setOrientation(orientation);

        // if the orientation has changed
        if (getOrientation() != oldOrientation) {

            // notify listeners of property change        
            firePropertyChange(
                ORIENTATION,
                oldOrientation,
                getOrientation());
        }
    }
    
    /**
     * Returns the current orientation value of the view collection.
     *
     * @see #setOrientation(int)
     */
    public int getOrientation() {
        return views.getOrientation();
    }
    
    /**
     * Sets the alignment of the length controls 
     * relative to the view collection 
     * to the given alignment value.
     *
     * If the alignment value is not valid,
     * the current alignment is not changed.
     *
     * @param alignment the new alignment of length controls 
     *      relative to the view collection
     * @see #getAlignment()
     * @see #ABOVE
     * @see #BELOW
     * @see #LEFT
     * @see #RIGHT
     * @see #DEFAULT
     */
    public void setAlignment(int alignment) {
        int oldAlignment = align;
    
        switch (alignment) {
        
            // check for valid alignment value
            case LEFT:
            case RIGHT:
            case ABOVE:
            case BELOW:
                align = alignment;
                break;
            
            // check for request for default alignment value
            case DEFAULT:
                align = DEFAULT_ALIGNMENT;
                break;
            
            // ignore invalid alignment values
            default:
                return;
        }
        
        // update the visualization
        revalidateDisplay();
        
        // if the alignment has changed
        if (align != oldAlignment) {

            // notify listeners of property change
            firePropertyChange(
                ALIGNMENT,
                oldAlignment,
                getAlignment());
        }
    }

    /**
     * Returns the alignment value of the length controls 
     * relative to the view collection.
     *
     * @see #setAlignment(int)
     * @see #ABOVE
     * @see #BELOW
     * @see #LEFT
     * @see #RIGHT
     */
    public int getAlignment() {
        return align;
    }
    
    /**
     * Sets the length controls for this array panel 
     * to those designated by the given value.
     *
     * If the given controls value is not valid,
     * only the appropriate controls information 
     * contained in the value is used.
     *
     * @param controls the sum of the values corresponding to 
     *      each of the desired controls
     * @see #getControls()
     * @see #NO_CONTROLS
     * @see #INCREMENT_DECREMENT
     * @see #LENGTH_TEXT_FIELD
     * @see #DEFAULT
     */
    public void setControls(int controls) {
        int oldControls = controlsValue;
        
        // check for request for default controls
        if (controls == DEFAULT)
            controlsValue = DEFAULT_CONTROLS;

        // ensure value contains only meaningful bits
        else
            controlsValue = controls & CONTROLS_MASK;

        // update the visualization
        revalidateDisplay();
            
        // if the controls have changed
        if (controlsValue != oldControls) {

            // notify listeners of property change
            firePropertyChange(
                CONTROLS,
                oldControls,
                getControls());            
        }
    }

    /**
     * Returns the controls value 
     * corresponding with the current length controls.
     *
     * @see #setControls(int)
     * @see #NO_CONTROLS
     * @see #INCREMENT_DECREMENT
     * @see #LENGTH_TEXT_FIELD
     */
    public int getControls() {
        return controlsValue;
    }

    /**
     * Sets the label for the length increment button 
     * to the given <code>String</code>.
     *
     * If the given <code>String</code> is <code>null</code>,
     * the button label is not changed.
     *
     * @param s the new label text for the length increment button
     * @see #getIncrementButtonText()
     */
    public void setIncrementButtonText(String s) {
        if (s == null)
            return;
    
        String oldText = getIncrementButtonText();

        increment.putValue(Action.NAME, s);

        // if the button text has changed
        if (getIncrementButtonText() != oldText) {

            // notify listeners of property change        
            firePropertyChange(
                INCREMENT_BUTTON_TEXT,
                oldText,
                getIncrementButtonText());
        }
    }

    /**
     * Returns the current label for the length increment button.
     *
     * @see #setIncrementButtonText(String)
     */
    public String getIncrementButtonText() {
        return (String)increment.getValue(Action.NAME);
    }

    /**
     * Sets the label for the length decrement button
     * to the given <code>String</code>.
     *
     * If the given <code>String</code> is <code>null</code>,
     * the button label is not changed.
     *
     * @param text the new label text 
     *      for the length decrement button
     * @see #getDecrementButtonText()
     */
    public void setDecrementButtonText(String text) {
        if (text == null)
            return;
    
        String oldText = getDecrementButtonText();

        decrement.putValue(Action.NAME, text);

        // if the button text has changed
        if (getDecrementButtonText() != oldText) {

            // notify listeners of property change        
            firePropertyChange(
                DECREMENT_BUTTON_TEXT,
                oldText,
                getDecrementButtonText());
        }
    }

    /**
     * Returns the current label for the length decrement button.
     *
     * @see #setDecrementButtonText(String)
     */
    public String getDecrementButtonText() {
        return (String)decrement.getValue(Action.NAME);
    }

    /**
     * Sets the prompt for the length field 
     * to the given <code>String</code>.
     *
     * If the given <code>String</code> is <code>null</code>,
     * the prompt text is not changed.
     *
     * @param prompt the new length field prompt text
     * @see #getLengthPrompt()
     */
    public void setLengthPrompt(String prompt) {
        if (prompt == null)
            return;
    
        String oldPrompt = getLengthPrompt();

        lengthControls.setAnnotationText(prompt);

        // if the prompt has changed
        if (getLengthPrompt() != oldPrompt) {

            // notify listeners of property change        
            firePropertyChange(
                LENGTH_PROMPT,
                oldPrompt,
                getLengthPrompt());
        }
    }

    /**
     * Returns the prompt text for the length field.
     *
     * @see #setLengthPrompt(String)
     */
    public String getLengthPrompt() {
        return lengthControls.getAnnotationText();
    }

    /**
     * Sets the label for the length set button
     * to the given <code>String</code>.
     *
     * If the given <code>String</code> is <code>null</code>,
     * the button label is not changed.
     *
     * @param text the new label text for the length set button
     * @see #getSetButtonText()
     */
    public void setSetButtonText(String text) {
        if (text == null)
            return;
    
        String oldText = getSetButtonText();

        set.putValue(Action.NAME, text);
        
        // if the button text has changed
        if (getSetButtonText() != oldText) {

            // notify listeners of property change        
            firePropertyChange(
                SET_BUTTON_TEXT,
                oldText,
                getSetButtonText());
        }
    }

    /**
     * Returns the current label text for the length set button.
     *
     * @see #setSetButtonText(String)
     */
    public String getSetButtonText() {
        return (String)set.getValue(Action.NAME);
    }

    /**
     * Sets the length of the array, 
     * that is, the number of views in the collection,
     * to the provided value.
     *
     * The given array length is assumed to be a raw length
     * and not a length provided from the user's perspective.
     *
     * This method ensures that the length is in the range
     * specified by the current minimum and maximum length
     * by increasing the given length to the minimum length
     * or decreasing the given length to the maximum length
     * as necessary.
     *
     * @param length the desired length of the array
     */
    public void setLength(int length) {
        int oldLength = getLength();
    
        // ensure provided length is in bounds
        if (length < minLength)
            length = minLength;
        
        if (length > maxLength)
            length = maxLength;
        
        // set lengthField
        int userLength = transformRawLengthToUserLength(length);
        lengthField.setViewState(userLength + "");
        
        // add elements if necessary
        while (getLength() < length)
            increment(null);

        // remove elements if necessary
        while (getLength() > length)
            decrement();

        // if the length has changed
        if (getLength() != oldLength) {

            // notify listeners of property change        
            firePropertyChange(
                LENGTH,
                oldLength,
                getLength());
        }
    }

    /**
     * Returns the current length of this array, 
     * that is, the number of views in this collection.
     */
    public int getLength() {
        return views.getItemCount();
    }
    
    /**
     * Sets the minimum length of this array to the given value.
     *
     * The given minimum length is assumed to be a raw length
     * and not a length provided from the user's perspective.
     *
     * If the given minimum length is less than zero,
     * the minimum length is set to zero.
     *
     * If the given minimum length is greater than the maximum length
     * the minimum length is set to the maximum length.
     *
     * @param length the minimum length
     * @see #getMinimumLength()
     * @see #setMaximumLength(int)
     * @see #getMaximumLength()
     */
    public void setMinimumLength(int length) {
        int oldMinLength = getMinimumLength();
        
        // enforce absolute minimum array length of 0
        if (length < 0)
            length = 0;
        
        // enforce: minimum length <= maximum length
       if (length > getMaximumLength())
           setMaximumLength(length);

        // set the minimum length to the provided value
        minLength = length;
        
        // set the lengthFilter
        userMinLength = transformRawLengthToUserLength(minLength);
        lengthFilter.setMinimum(userMinLength);
        
        // enable the decrement button if appropriate
        if (getLength() > minLength)
            decrement.setEnabled(isEnabled());
        
        // otherwise disable the decrement button    
        else {
            decrement.setEnabled(false);

            // add new elements if necessary
            if (getLength() < minLength)
                setLength(minLength);
        }

        // if the minimum length has changed
        if (getMinimumLength() != oldMinLength) {

            // notify listeners of property change        
            firePropertyChange(
                MINIMUM_LENGTH,
                oldMinLength,
                getMinimumLength());
        }
    }

    /**
     * Returns the minimum length of this array.
     *
     * @see #setMinimumLength(int)
     * @see #getMaximumLength()
     * @see #setMaximumLength(int)
     */
    public int getMinimumLength() {
        return minLength;
    }
    
    /**
     * Sets the maximum length of this array to the given value.
     *
     * The given maximum length is assumed to be a raw length
     * and not a length provided from the user's perspective.
     *
     * If the given maximum length is less than the minimum length,
     * the minimum length is set to the maximum length.
     *
     * @param length the maximum length
     * @see #getMaximumLength()
     * @see #setMinimumLength(int)
     * @see #getMinimumLength()
     */
    public void setMaximumLength(int length) {
        int oldMaxLength = getMaximumLength();
        
        // enforce absolute minimum array length of 0
        if (length < 0)
            length = 0;
        
        // enforce: maximum length >= minimum length
        if (length < getMinimumLength())
            setMinimumLength(length);

        // set the maximum length to the provided value
        maxLength = length;
        
        // set the lengthFilter
        userMaxLength = transformRawLengthToUserLength(maxLength);
        lengthFilter.setMaximum(userMaxLength);
        
        // enable the increment button if appropriate
        if (getLength() < maxLength)
            increment.setEnabled(isEnabled());
    
        // otherwise disable the increment button    
        else {
            increment.setEnabled(false);

            // remove elements if necessary
            while (getLength() > maxLength)
                setLength(maxLength);
        }

        // if the maximim length has changed
        if (getMaximumLength() != oldMaxLength) {

            // notify listeners of property change        
            firePropertyChange(
                MAXIMUM_LENGTH,
                oldMaxLength,
                getMaximumLength());
        }
    }

    /**
     * Returns the maximum length for this array.
     *
     * @see #setMaximumLength(int)
     * @see #getMinimumLength()
     * @see #setMinimumLength(int)
     */
    public int getMaximumLength() {
        return maxLength;
    }
    
    ///////////////////////
    // Protected methods //
    ///////////////////////
    
    /**
     * Returns a display settings object 
     * appropriate for the element at the given index.
     *
     * By default this method returns a settings object
     * encapsulating prompt text created by the
     * <code>{@link #createPromptTextFor(int) 
     *              createPromptTextFor}</code> method,
     * the default prompt alignment, and no title.
     *
     * Derived classes may override this method and produce
     * display settings appropriate for input of elements 
     * of a specific type.
     *
     * @param index the index of the element
     * @see #createViewFor(int)
     * @see #createPromptTextFor(int)
     * @see "Building a custom ArrayPanel"
     */
    protected Display.Settings createDisplaySettingsFor(int index) {
        return new Display.Settings(
            createPromptTextFor(index),
            null, 
            Display.LEFT, 
            Display.DEFAULT);
    }

    /**
     * Returns an input component suitable for the element 
     * at the given index in this array.
     *
     * By default this method returns a <code>TextFieldView</code>
     * with the empty string as its default view state.
     *
     * Derived classes may override this method and produce
     * views appropriate for input of elements of a specific type.
     *
     * @param index the index of the element 
     * @see #createDisplaySettingsFor(int)
     * @see #createPromptTextFor(int)
     * @see "Building a custom ArrayPanel"
     */
    protected TypedView createViewFor(int index) {
        return new TextFieldView("");
    }
    
    /**
     * Returns prompt text suitable for the element 
     * at the given index in this array.
     *
     * By default, this method returns 
     * a <code>String</code> containing the index of the element
     * followed by a colon.
     *
     * Derived classes should override this method and produce
     * prompt text appropriate for input of elements of a specific 
     * type.
     *
     * @param index the index of the element
     * @see #createDisplaySettingsFor(int)
     * @see #createViewFor(int)
     * @see "Building a custom ArrayPanel"
     */
    protected String createPromptTextFor(int index) {
        return index + ":";
    }
    
    /**
     * <p>Returns an error message indicating that the user
     * attempted to set the array length to a value that is
     * less than the minimum array length or greater than
     * the maximum array length.</p>
     *
     * <p>By default this method returns a <code>String</code>
     * with an appropriate error message using the text
     * returned by getLengthPrompt() to make the message more
     * specific.</p>
     *
     * @param exception the filter exception that was generated
     *      by the filter policing the array length bounds
     */
    public String createLengthSetOutOfBoundsMessage(
        FilterException exception) 
    {
        // try to return the most appropriate message
        try {
            if (exception.getModel() instanceof XNumber) {
                XNumber x = (XNumber)exception.getModel();
                int length = 
                    transformUserLengthToRawLength(x.intValue());
                
                // return a message indicating 
                // that the length violates the minimum bound
                if (length < minLength) {
                    return "The given "
                          + getLengthPrompt()
                          + " is less than the minimum "
                          + getLengthPrompt()
                          + " of "
                          + transformRawLengthToUserLength(minLength);
                }
                
                // otherwise return a message indicating 
                // that the length violates the maximum bound
                    return "The given "
                          + getLengthPrompt()
                          + " is greater than the maximum "
                          + getLengthPrompt()
                          + " of "
                          + transformRawLengthToUserLength(maxLength);
            }
        }
        catch (NumberFormatException ex) {}
        
        // if an appropriate message cannot be chosen,
        // return the absolute default message
        return "The provided "
              + getLengthPrompt()
              + " is not in the range ["
              + transformRawLengthToUserLength(minLength)
              + ", "
              + transformRawLengthToUserLength(maxLength)
              + "]";
    }

    /**
     * Appends an appropriate input component 
     * to the end of the view collection 
     * whose view state is initialized from the given data
     * <code>String</code>.  
     *
     * If <code>null</code>, the provided data is ignored 
     * and the default view state for the created view is used.
     *
     * This method assumes that the current array length 
     * is less than the maximum length of the array.
     * 
     * @param data the desired data state for the element
     */
    protected void increment(String data) {

        // add a new view to the collection
        Component c = createDisplayFor(getLength());
        views.add(c);
        
        decrement.setEnabled(isEnabled());
        if (getLength() == maxLength)
            increment.setEnabled(false);
        
        // set the value for the view if appropriate
        if (data != null) {
            Displayable d = (Displayable)c;
            d.setViewState(data);
        }
    }
    
    /**
     * Removes the last input component 
     * from the end of the view collection.
     *
     * This method assumes that the current array length 
     * is greater than the minimum length of the array.
     *
     * @return the removed input component
     */
    protected TypedView decrement() {
    
        // remove the last display from the collection    
        Component c = views.getItem(getLength() - 1);
        views.remove(c);
        
        // store the typed view that it contains
        Display d = (Display)c;
        TypedView v = (TypedView)d.getDisplay();

        increment.setEnabled(isEnabled());
        if (getLength() == minLength)
            decrement.setEnabled(false);

        // return the typed view
        return v;
    }

    /**
     * Sets the length of the array, 
     * that is, the number of views in the collection,
     * to the raw array length represented
     * by the view state of the length field.
     *
     * If the view state of the length field is malformed
     * and the user cancels error handling for the malformed data,
     * the current array length will not be changed.
     */
    protected void applyLengthFromControls() {

        // extract the raw array length represented by
        // the view state of the length field
        int length;
        
        try {
            length = requestArrayLength();
        }

        // quit the length set operation if the user cancels
        catch (CancelledException ex) {
            return;
        }

        setLength(length);
    }

    /**
     * Returns the value contained in the length field
     * after applying the filter that polices
     * the minimum and maximum array length
     * and transforming the value from the user length perspective
     * to the raw length perspective.
     *
     * @throws CancelledException
     *      if the user cancels the set length operation
     */
    protected int requestArrayLength()
        throws CancelledException
    {
        return transformUserLengthToRawLength(
            lengthField.requestInt(lengthFilter));
    }

    /**
     * Returns the raw array length corresponding
     * to the given array length from the user perspective.
     *
     * By default, this method returns the given value.
     *
     * Derived classes may override this method in order
     * to allow the user to have a mental model of the array length
     * that differs from the raw length of this array.
     *
     * @param userLength a length of this array 
     *      from the user perspective
     */
    protected int transformUserLengthToRawLength(int userLength) {
        return userLength;
    }

    /**
     * Returns the array length from the user perspective
     * corresponding to the given raw array length.
     *
     * By default, this method returns the given value.
     *
     * Derived classes may override this method in order
     * to allow the raw length of the array to differ from 
     * the user's mental model of the array length.
     *
     * @param rawLength a raw length of this array
     */
    protected int transformRawLengthToUserLength(int rawLength) {
        return rawLength;
    }

    /**
     * Updates the contents and layout of the control panel
     * based on the current parameters.
     */
    protected void revalidateDisplay() {

        // remove old control panel
        if (controlPanel != null)
            remove(controlPanel);

        // create new control panel            
        controlPanel = createControlPanel();

        // add the control panel to the display if necessary
        if (controlPanel != null) {
            add(controlPanel, 
                JPTUtilities.getBorderLayoutLocation(align));
        }
        
        // update the visualization
        revalidate();
    }

    /**
     * Creates a length control panel with the appropriate controls.
     */
    protected DisplayPanel createControlPanel() {

        // return null if no control panel is needed
        if (controlsValue == NO_CONTROLS)
            return null;
            
        // build a panel to hold the controls
        DisplayPanel temp = new DisplayPanel();
        temp.setLayout(new BoxLayout(temp, BoxLayout.Y_AXIS));
            
        // add the appropriate controls to the panel
        if ((controlsValue & INCREMENT_DECREMENT) != 0) {
            temp.add(lengthActions);
        }
        
        if ((controlsValue & LENGTH_TEXT_FIELD) != 0) {
            JPanel p = new JPanel(new FlowLayout(FlowLayout.CENTER));
                p.add(lengthControls);
                p.add(setActions);
            temp.add(p);
        }

        // ensure controls don't stretch to fill empty space
        temp.add(Box.createVerticalGlue());

        // return the created panel
        return temp;
    }

    /////////////////////
    // Private methods //
    /////////////////////

    /**
     * Creates an appropriate display for an element at the given
     * index by combining an appropriate view object with the
     * appropriate prompt text, using a display object with the
     * appropriate settings.
     *
     * @param index the index of the desired element
     */
    private final Display createDisplayFor(int index) {
        return new Display(
            createViewFor(index),
            createDisplaySettingsFor(index));
    }
}
