
package uk.co.wingpath.gui;

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

/**
* This class provides an alternative field for entering and editing long
* strings in text fields and table cells.
* Text components that use a MirrorField must provide an implementation of
* the {@link MirroredField} interface.
* <p>A {@code MirroredField} may be in one of three states with respect to
* the mirror:
* <ul>
*   <li>The mirrored field is not associated with the mirror. The text
*   component should call
*   {@link MirrorField#setMirrored MirrorField.setMirrored}
*   when the user selects the text component in order to associate itself
*   with the mirror.
*   <li>The text component is handling the editing, with changes copied to the
*   mirror field by calling {@link #displayValue displayValue}.
*   <li>The mirror field is handling the editing. The mirror will call the
*   methods defined by the {@link MirroredField} interface to notify the
*   mirrored field of changes made by the user.
* </ul>
* <p>Editing using the mirror is finished when the mirror loses focus or the
* user presses Enter, Tab or Escape. If the user presses Escape, the mirror
* will call {@link MirroredField#resetValue MirroredField.resetValue},
* otherwise the mirror will call {@link MirroredField#saveValue
* MirroredField.saveValue}. The text component is no longer
* associated with the mirror after resetValue or saveValue has been
* called, although if the user switches focus back to the text component it
* may re-associate itself.
*/
public class MirrorField
{
    private final JPanel panel;
    private final JTextField field;
    private final JLabel label;
    private MirroredField mirrored;
    private boolean hasFocus;

    /**
    * Constructs a MirrorField.
    * The MirrorField is initially unassociated and disabled.
    */
    public MirrorField ()
    {
        mirrored = null;
        hasFocus = false;
        panel = new JPanel ();
        panel.setLayout (new BoxLayout (panel, BoxLayout.X_AXIS));
        label = new JLabel ("");
        label.setForeground (Color.BLUE);
        panel.add (label);
        field = new JTextField (40);
        panel.add (field);
        field.setEnabled (false);
        field.setFocusTraversalKeysEnabled (false);
        InputMap inputMap = field.getInputMap (JComponent.WHEN_FOCUSED);
        inputMap.put (KeyStroke.getKeyStroke (KeyEvent.VK_ESCAPE, 0), "cancel");
        inputMap.put (KeyStroke.getKeyStroke (KeyEvent.VK_TAB, 0), "ok");
        inputMap.put (KeyStroke.getKeyStroke (KeyEvent.VK_ENTER, 0), "ok");
        inputMap.put (KeyStroke.getKeyStroke (KeyEvent.VK_F2, 0), "ok");

        int width = panel.getMaximumSize ().width;
        int height = field.getMinimumSize ().height;
        panel.setMaximumSize (new Dimension (width, height));

        field.setInputVerifier (
            new InputVerifier ()
            {
                @Override
                public boolean verify (JComponent component)
                {
                    return MirrorField.this.verify (field.getText ()) != null;
                }
            });

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

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

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

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

                public void focusLost (FocusEvent e)
                {
                    if (e.isTemporary () || !hasFocus)
                        return;
                    hasFocus = false;
                    if (mirrored != null)
                    {
                        // The InputVerifier should have checked the value
                        // for validity before allowing focus to be lost.
                        String val = verify (field.getText ());
                        assert val != null;
                        mirrored.saveValue (val);
                        setMirrored (null);
                    }
                }
            });

        ActionMap actionMap = field.getActionMap ();
        actionMap.put ("ok",
            new AbstractAction ()
            {
                public void actionPerformed (ActionEvent ev)
                {
                    if (mirrored != null)
                    {
                        String val = verify (field.getText ());
                        if (val != null)
                        {
                            mirrored.saveValue (val);
                            mirrored.restoreFocus ();
                            setMirrored (null);
                        }
                    }
                }
            });

        actionMap.put ("cancel",
            new AbstractAction ()
            {
                public void actionPerformed (ActionEvent ev)
                {
                    if (mirrored != null)
                    {
                        field.setText (mirrored.resetValue ());
                        mirrored.restoreFocus ();
                        setMirrored (null);
                    }
                }
            });
    }

    private String verify (String val)
    {
        if (mirrored == null)
            return val;
        Verifier verifier = mirrored.getVerifier ();
        if (verifier == null)
            return val;
        return verifier.verify (val, false);
    }

    /**
    * Gets the Swing component for displaying the mirror field.
    * @return the mirror Swing component.
    */
    public JComponent getComponent ()
    {
        return panel;
    }

    /**
    * Sets the {@link MirroredField} to be associated with this mirror.
    * @param mirrored the mirrored field. May be null to remove an association.
    */
    public void setMirrored (MirroredField mirrored)
    {
        this.mirrored = mirrored;
        if (mirrored == null)
        {
            field.setEnabled (false);
            field.setText ("");
            label.setText ("");
        }
        else
        {
            field.setEnabled (true);
            String l = mirrored.getMirrorLabel ();
            label.setText ((l == null || l.equals ("")) ? "" : l + ": ");
        }
    }

    /**
    * Tests whether the specified {@link MirroredField} is associated with
    * this mirror.
    * @param mirrored the MirroredField to be tested.
    * @return true if the specified MirroredField is associated with the mirror.
    */
    public boolean isMirrored (MirroredField mirrored)
    {
        return this.mirrored == mirrored;
    }

    private void documentUpdate ()
    {
        if (hasFocus && mirrored != null)
        {
            String val = field.getText ();
            // Call the verifier to clear any error message.
            Verifier verifier = mirrored.getVerifier ();
            if (verifier != null)
                verifier.verify (val, true);
        }
    }

    /**
    * Updates the value displayed by the mirror field.
    * @param val the value to be displayed.
    */
    public void displayValue (String val)
    {
        if (hasFocus)
        {
            throw new IllegalStateException (
                "displayValue called when hasFocus");
        }
        if (!val.equals (field.getText ()))
        {
            field.setText (val);
        }
    }

    public boolean isFocusTarget (Component component)
    {
        return component == field;
    }

    public void requestFocus ()
    {
        field.requestFocusInWindow ();
    }
}

