001/*
002 * Created on 23.04.2009
003 *
004 */
005package org.jdesktop.swingx.event;
006
007import java.awt.Component;
008import java.awt.KeyboardFocusManager;
009import java.beans.PropertyChangeEvent;
010import java.beans.PropertyChangeListener;
011
012import javax.swing.JComponent;
013
014import org.jdesktop.beans.AbstractBean;
015import org.jdesktop.swingx.SwingXUtilities;
016import org.jdesktop.swingx.util.Contract;
017
018/**
019 * An convenience class which maps focusEvents received
020 * from a container hierarchy to a bound read-only property. Registered
021 * PropertyChangeListeners are notified if the focus is transfered into/out of
022 * the hierarchy of a given root.
023 * <p>
024 * 
025 * F.i, client code which wants to get notified if focus enters/exits the hierarchy below
026 * panel would install the compound focus listener like:
027 * 
028 * <pre>
029 * <code>
030 *         // add some components inside
031 *         panel.add(new JTextField(&quot;something to .... focus&quot;));
032 *         panel.add(new JXDatePicker(new Date()));
033 *         JComboBox combo = new JComboBox(new Object[] {&quot;dooooooooo&quot;, 1, 2, 3, 4 });
034 *         combo.setEditable(true);
035 *         panel.add(new JButton(&quot;something else to ... focus&quot;));
036 *         panel.add(combo);
037 *         panel.setBorder(new TitledBorder(&quot;has focus dispatcher&quot;));
038 *         // register the compound dispatcher
039 *         CompoundFocusListener report = new CompoundFocusListener(panel);
040 *         PropertyChangeListener l = new PropertyChangeListener() {
041 * 
042 *             public void propertyChange(PropertyChangeEvent evt) {
043 *                 // do something useful here
044 *                 
045 *             }};
046 *         report.addPropertyChangeListener(l);    
047 *         
048 * </code>
049 * </pre>
050 * 
051 * PENDING JW: change of current instance of KeyboardFocusManager?
052 * 
053 */
054public class CompoundFocusListener extends AbstractBean {
055    
056    /** the root of the component hierarchy. 
057     * PENDING JW: weak reference and auto-release listener? 
058     */ 
059    private JComponent root;
060    /** PropertyChangeListener registered with the current keyboardFocusManager. */
061    private PropertyChangeListener managerListener;
062    private boolean focused;
063
064    /**
065     * Instantiates a CompoundFocusListener on the component hierarchy below the given
066     * component. 
067     * 
068     * @param root the root of a component hierarchy
069     * @throws NullPointerException if the root is null
070     */
071    public CompoundFocusListener(JComponent root) {
072        this.root = Contract.asNotNull(root, "root must not be null");
073        KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
074        addManagerListener(manager);
075        permanentFocusOwnerChanged(manager.getPermanentFocusOwner());
076    }
077
078
079    /**
080     * Return true if the root or any of its descendants is focused. This is a
081     * read-only bound property, that is property change event is fired if focus
082     * is transfered into/out of root's hierarchy.
083     * 
084     * @return a boolean indicating whether or not any component in the
085     *         container hierarchy below root is permanent focus owner.
086     */
087    public boolean isFocused() {
088        return focused;
089    }
090
091    /**
092     * Releases all listeners and internal references.<p>
093     * 
094     * <b>Note</b>: this instance must not be used after calling this method.
095     *  
096     */
097    public void release() {
098        removeManagerListener(KeyboardFocusManager.getCurrentKeyboardFocusManager());
099        removeAllListeners();
100        this.root = null;
101    }
102    
103    /**
104     * Removes all property change listeners which are registered with this instance.
105     */
106    private void removeAllListeners() {
107        for (PropertyChangeListener l : getPropertyChangeListeners()) {
108            removePropertyChangeListener(l);
109        }
110    }
111
112    /**
113     * Updates focused property depending on whether or not the given component
114     * is below the root's hierarchy. <p>
115     * 
116     * Note: Does nothing if the component is null. This might not be entirely correct,
117     * but property change events from the focus manager come in pairs, with only 
118     * one of the new/old value not-null. 
119     * 
120     * @param focusOwner the component with is the current focusOwner.
121     */
122    protected void permanentFocusOwnerChanged(Component focusOwner) {
123        if (focusOwner == null) return;
124        setFocused(SwingXUtilities.isDescendingFrom(focusOwner, root));
125    }
126    
127    private void setFocused(boolean focused) {
128        boolean old = isFocused();
129        this.focused = focused;
130        firePropertyChange("focused", old, isFocused());
131    }
132
133    
134    /**
135     * Adds all listeners to the given KeyboardFocusManager. <p>
136     * 
137     * @param manager the KeyboardFocusManager to add internal listeners to.
138     * @see #removeManagerListener(KeyboardFocusManager)
139     */
140    private void addManagerListener(KeyboardFocusManager manager) {
141        manager.addPropertyChangeListener("permanentFocusOwner", getManagerListener());
142    }
143
144    /**
145     * Removes all listeners this instance has installed from the given KeyboardFocusManager.<p>
146     * 
147     * @param manager the KeyboardFocusManager to remove internal listeners from.
148     * @see #addManagerListener(KeyboardFocusManager)
149     */
150    private void removeManagerListener(KeyboardFocusManager manager) {
151        manager.removePropertyChangeListener("permanentFocusOwner", getManagerListener());
152    }
153
154    /**
155     * Lazily creates and returns a property change listener to be registered on the
156     * KeyboardFocusManager.
157     * 
158     * @return a property change listener to be registered on the KeyboardFocusManager.
159     */
160    private PropertyChangeListener getManagerListener() {
161        if (managerListener == null) {
162            managerListener = new PropertyChangeListener() {
163
164                @Override
165                public void propertyChange(PropertyChangeEvent evt) {
166                    if ("permanentFocusOwner".equals(evt.getPropertyName())) {
167                        permanentFocusOwnerChanged((Component) evt.getNewValue());
168                    }
169                    
170                }};
171        }
172        return managerListener;
173    }
174
175
176}