/**
 * ComponentDependencyHandler.java
 * copyright 2001, 2006 by Melinda Green
 * Superliminal Software
 * 
 * This class is meant to help avoid the confusion about the different types
 * of listeners the different Swing and AWT components support. Additional
 * listener interfaces can be easily added. It also helps solve the perennial
 * problem of components being enabled or disabled, visible or invisible etc.
 * at the wrong times.
 * 
 * ComponentDependencyHandler objects add themselves as listeners to a given 
 * set of components. Then when any of those components generate any of the 
 * usual events, the ComponentDependencyHandler object calls its 
 * dependencyNotification method. Users only need to implement that abstract 
 * method and update whatever state of its dependent components may need to 
 * change, without having to worry about which type of event occurred.
 * 
 * Applications will typically create anonymous subclasses of this class and
 * implement the abstract dependencyNotification method to set whatever state
 * may need to change in response to state changes in their dependent components.
 * For example the following code shows how to create a dependency between two
 * buttons such that the user can only select the "Next Page" button once they've
 * indicated that they've read some license agreement by clicking an "I Accept"
 * button. In other words, the "enabled" property of one component is made to be
 * dependent upon the "selected" property of another component:
 * 
 * final JToggleButton accept_button = new JToggleButton("I Accept");
 * final JButton next_page = new JButton("Next Page");
 * next_page.setEnabled(false);
 * // create ComponentDependencyHandler that enforces that the next_page button 
 * // is only enabled when the accept_button is selected:
 * new ComponentDependencyHandler(accept_button) {
 *     public void dependencyNotification() {
 *         next_page.setEnabled(accept_button.isSelected());
 *     }
 * }
 * 
 * The dependencyNotification method takes no arguments. It's therefore assumed that
 * your callback code has access to the components it's dependent upon and can
 * inquire the states of those dependencies in order to make appropriate state
 * changes to the target component or components as shown above. Note that your
 * callback method will likely be called more often than needed since it will be
 * triggered by any number of state changes in its dependent objects, but since these
 * events will likely be due to user actions that should almost never be a problem.
 * Note also that the new ComponentDependencyHandler object wasn't even saved in a
 * local variable. It will simply exist on the heap only referenced by the components
 * it's listening to. It exists only to keep some component states in synch according
 * to your business logic.
 * 
 * Using this pattern, dependency graphs of arbitrary complexity can be generated
 * which are easily maintained. The test code of the main method here shows a non-
 * trivial example using this dataflow pattern.
 */

package com.superliminal.uiutil;

import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.*;
import java.beans.*;

public abstract class ComponentDependencyHandler implements 
    ActionListener, 
    ChangeListener, 
    ItemListener, 
    ListSelectionListener,
    PropertyChangeListener,
    FocusListener,
    KeyListener
{
    // The control list of events we want to respond to.
    // Just add or remove listener classes and recompile.
    private static Class listener_classes[] = {
        ActionListener.class,
        ItemListener.class,
        ChangeListener.class,
        ListSelectionListener.class,
        PropertyChangeListener.class,
        FocusListener.class,
        KeyListener.class,
    };
    
    /**
     * Constructs a ComponentDependencyHandler with a set of dependent components.
     */
    public ComponentDependencyHandler(Component... dependents) {
        for(Component comp : dependents)
            for(Class cls : listener_classes)
                tryToAddListener(comp, cls);
    }
    
    /**
     * User subclasses only need to implement this method to receive
     * notification of changes to dependent components.
     */
    public abstract void dependencyNotification();

    private void tryToAddListener(Component comp, Class listener_class) {
        String listener_name = listener_class.getName();
        int last_dot = listener_name.lastIndexOf('.');
        listener_name = listener_name.substring(last_dot+1);
        try {
            Method adder = comp.getClass().getMethod("add" + listener_name, listener_class);
            adder.invoke(comp, this);
        }
        catch(NoSuchMethodException nsme){} // getMethod
        catch(IllegalAccessException iae){} // invoke
        catch(InvocationTargetException ite){} // invoke
    }

    // all the listener implementations that all forward notification to subclasses
    public void stateChanged(ChangeEvent e) { dependencyNotification();}
    public void itemStateChanged(ItemEvent e) { dependencyNotification(); }
    public void valueChanged(ListSelectionEvent e) { dependencyNotification(); }
    public void actionPerformed(ActionEvent e) { dependencyNotification(); }
    public void propertyChange(PropertyChangeEvent e) { dependencyNotification(); }
    public void focusLost(FocusEvent e) { dependencyNotification(); }
    public void focusGained(FocusEvent e) { dependencyNotification(); }
    public void keyTyped(KeyEvent e) { dependencyNotification(); }
    public void keyPressed(KeyEvent e) { dependencyNotification(); }
    public void keyReleased(KeyEvent e) { dependencyNotification(); }


    /**
     * A simple example program.
     */
    public static void main(String args[]) {
        // build the components 
        final JLabel
            use_password_label = new JLabel("Use Password"),
            enter_label = new JLabel("Enter Password:"),
            instruction_label = new JLabel("Hit <space> to toggle checkbox");
        final JCheckBox password_checkbox = new JCheckBox();
        final JTextField password_text = new JTextField("");
        final JButton submit_button = new JButton("Submit");
        
        // set initial component states
        enter_label.setVisible(false);
        instruction_label.setVisible(false);
        
        // Now set up the dependencies.
        // This is the meat of the example. Notice that the ComponentDependencyHandler 
        // objects only need to be created in order to work. Each one implements
        // the business logic associated with changes to the states of the
        // component or components given to it's constructor.
        
        // "enter" text visibility depends upon checkbox selection
        new ComponentDependencyHandler(password_checkbox) {
            public void dependencyNotification() {
                enter_label.setVisible(password_checkbox.isSelected());
            }
        };
        // text entry field enabled depends upon checkbox selection
        new ComponentDependencyHandler(password_checkbox) {
            public void dependencyNotification() {
                password_text.setEnabled(password_checkbox.isSelected());
            }
        };      
        // instruction text visibility depends upon checkbox FOCUS
        new ComponentDependencyHandler(password_checkbox) {
            public void dependencyNotification() {
                instruction_label.setVisible(password_checkbox.hasFocus());
            }
        };      
        // "select" button enabling depends upon checkbox selected AND password text
        new ComponentDependencyHandler(password_checkbox, password_text) {
            public void dependencyNotification() {
                submit_button.setEnabled(
                    password_checkbox.isSelected()
                    && password_text.getText().length() > 0
                );
            }
        };
        
        // build and display the dialog
        JPanel first_row = new JPanel();
        first_row.setLayout(new BoxLayout(first_row, BoxLayout.X_AXIS));
        first_row.add(use_password_label);
        first_row.add(password_checkbox);
        first_row.add(enter_label);
        first_row.add(password_text);
        JFrame password_dialog = new JFrame("ComponentDependencyHandler Test");
        Container cont = password_dialog.getContentPane();
        cont.setLayout(new BorderLayout());
        cont.add("North", first_row);
        cont.add("Center", instruction_label);
        cont.add("South", submit_button);
        password_dialog.setSize(300, 100);
        password_dialog.setVisible(true);
        password_dialog.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}