
package uk.co.wingpath.gui;

import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import uk.co.wingpath.event.*;
import uk.co.wingpath.event.Event;
import uk.co.wingpath.util.*;


/**
* This class implements {@code WComponent<String>} using an editable
*  <code>JComboBox</code>.
* <p>A {@code WEditableComboBox} is best thought of as an enhanced
* {@code WTextField}, which allows the user to enter text by typing it or
* by selecting it from a from a list.
* <p>{@code WEditableComboBox} can be used as a plug-in alternative to
* {@code WTextField}.
*/
public class WEditableComboBox
    extends WAbstractComponent<String>
    implements Verifiable
{
    private final JComboBox comboBox;
    private final JTextField field;
    private boolean isChanging;
    private boolean hasFocus;
    private boolean modifying;
    private Verifier verifier;
    private String value;

    /**
    * Constructs an initially empty <code>WEditableComboBox</code>.
    * @param label text for the associated label, or <code>null</code> if
    * there is no associated label.
    */
    public WEditableComboBox (StatusBar statusBar, String label)
    {
        Event.checkIsEventDispatchThread ();
        comboBox = new JComboBox ();
        comboBox.setEditable (true);
        field = (JTextField) comboBox.getEditor ().getEditorComponent ();
        value = "";
        isChanging = false;
        hasFocus = false;
        modifying = false;
        verifier = new NullVerifier (statusBar);
        initialize (comboBox, label);

        field.setInputVerifier (new InputVerifier ()
        {
            public boolean verify (JComponent component)
            {
                if (modifying)
                    return true;
                return checkValue ();
            }
        });

        field.getDocument ().addDocumentListener (new DocumentListener ()
        {
            public void insertUpdate (DocumentEvent e)
            {
                documentUpdate ();
            }

            public void removeUpdate(DocumentEvent e)
            {
                documentUpdate ();
            }

            public void changedUpdate(DocumentEvent e)
            {
            }
        });

        comboBox.addActionListener (new ActionListener ()
        {
            // An ActionEvent may occur for one of the following reasons:
            //  1. the user selected an item from the list.
            //  2. the user has finished editing (by pressing Enter)
            //  3. the program has selected an item.
            //  4. the program has added/removed an item (which causes a
            //     selection change).
            // The last two cases are ignored by testing the 'modifying'
            // flag. In the first case, we do a redundant verifier call.
            public void actionPerformed (ActionEvent e)
            {
                if (!modifying)
                    checkValue ();
            }
        });

        field.addFocusListener (new FocusListener ()
        {
            public void focusGained (FocusEvent e)
            {
                if (!hasFocus)
                {
                    field.selectAll ();
                    hasFocus = true;
                }
            }

            public void focusLost (FocusEvent e)
            {
                if (!e.isTemporary ())
                    hasFocus = false;
            }
        });
    }

    private void documentUpdate ()
    {
        // Calling 'setText' fires DocumentEvents, so we can't assume that
        // a DocumentEvent is caused by the user editing the value.
        // So test the value to see if the user is changing it.
        if (modifying)
            return;
        String val = field.getText ().trim ();
        boolean wasChanging = isChanging;
        isChanging = !val.equals (value);
        if (isChanging != wasChanging)
        {
            if (listeners != null)
                listeners.fireValueChanged (this, isChanging);
        }

        // Call the verifier to clear any error message.
        if (verifier != null)
            verifier.verify (val, true);
    }

    /**
     * Sets the horizontal alignment of the selection text.
     * Valid parameter values are:
     * <ul>
     * <li><code>SwingConstants.LEFT</code>
     * <li><code>SwingConstants.CENTER</code>
     * <li><code>SwingConstants.RIGHT</code>
     * <li><code>SwingConstants.LEADING</code>
     * <li><code>SwingConstants.TRAILING</code>
     * </ul>
     * @param alignment the required alignment
     */
    @Override
    public void setAlignment (int alignment)
    {
        Event.checkIsEventDispatchThread ();
        ListCellRenderer renderer = comboBox.getRenderer ();
        if (renderer instanceof JLabel)
            ((JLabel) renderer).setHorizontalAlignment (alignment);
    }

    /**
    * Checks whether the value that the user has entered is valid.
    * <p>If the value is valid, it is stored as the value of the component,
    * a value event is fired, and <code>true</code> is returned.
    * If the value is not valid, an error
    * message is displayed and <code>false</code> is returned.
    * <p>If the user is not editing the value, <code>true</code> is returned.
    * @return <code>false</code> if the user has entered an invalid value,
    * <code>true</code> otherwise.
    */
    @Override
    public boolean checkValue ()
    {
        Event.checkIsEventDispatchThread ();
        if (!isChanging)
            return true;

        String val = field.getText ().trim ();
        if (verifier != null)
        {
            val = verifier.verify (val, false);
            if (val == null)
                return false;
        }

        isChanging = false;
        value = val;
        field.setText (value);
        if (listeners != null)
            listeners.fireValueChanged (this, false);
        return true;
    }

    /**
    * Sets the verifier for this component.
    * @param verifier the new verifier
    * @see Verifier
    */
    public void setVerifier (Verifier verifier)
    {
        this.verifier = verifier;
    }

    /**
    * Gets the value of the component.
    * @return the value of the component.
    */
    public String getValue ()
    {
        return value;
    }

    /**
    * Sets the value of the component.
    * <p>This method does NOT fire a value event.
    * @param val the new value for the component.
    */
    public void setValue (String val)
    {
        Event.checkIsEventDispatchThread ();
        if (isChanging || !val.equals (value))
        {
            modifying = true;
            value = val;
            field.setText (value);
            comboBox.setSelectedItem (value);
            isChanging = false;
            modifying = false;
            return;
        }
    }

    @Override
    public boolean hasValueChanged (String oldValue)
    {
        boolean changed = false;
        if (isChanging)
            changed = true;
        if (!oldValue.equals (field.getText ().trim ()))
            changed = true;
        field.setBackground (changed ? Gui.COLOUR_BACKGROUND_UNAPPLIED :
            Gui.COLOUR_BACKGROUND_NORMAL);
        return changed;
    }

    /**
    * Sets the combobox list of items from the specified collection.
    * @param items the items to be set.
    */
    public void setItems (Collection<String> items)
    {
        Event.checkIsEventDispatchThread ();
        modifying = true;
        comboBox.removeAllItems ();

        for (String item : items)
        {
            comboBox.addItem (item);
        }

        comboBox.setSelectedItem (value);
        isChanging = false;
        modifying = false;
    }

    /**
    * Sets the combobox list of items from the specified array.
    * @param items the items to be set.
    */
    public void setItems (String [] items)
    {
        Event.checkIsEventDispatchThread ();
        modifying = true;
        comboBox.removeAllItems ();

        for (String item : items)
        {
            comboBox.addItem (item);
        }

        comboBox.setSelectedItem (value);
        isChanging = false;
        modifying = false;
    }

    /**
    * Adds a PopupMenuListener.
    * @param l the PopupMenuListener to add.
    */
    public void addPopupMenuListener (PopupMenuListener l)
    {
        comboBox.addPopupMenuListener (l);
    }

    /**
    * Removes a PopupMenuListener.
    * @param l the PopupMenuListener to remove.
    */
    public void removePopupMenuListener (PopupMenuListener l)
    {
        comboBox.removePopupMenuListener (l);
    }
}

