001/*
002 * $Id: TargetManager.java 3972 2011-03-17 20:31:58Z kschaefe $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 * 
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 * 
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021
022package org.jdesktop.swingx.action;
023
024import java.awt.Component;
025import java.awt.KeyboardFocusManager;
026import java.awt.event.ActionEvent;
027import java.beans.PropertyChangeListener;
028import java.beans.PropertyChangeSupport;
029import java.util.ArrayList;
030import java.util.Iterator;
031import java.util.List;
032
033import javax.swing.Action;
034import javax.swing.ActionMap;
035import javax.swing.JComponent;
036
037
038
039/**
040 * The target manager dispatches commands to {@link Targetable} objects
041 * that it manages. This design of this class is based on the <i>Chain of
042 * Responsiblity</i> and <i>Mediator</i> design patterns. The target manager
043 * acts as a mediator between {@link TargetableAction}s and the intended targets.
044 * This allows Action based components to invoke commands on components
045 * without explicitly binding the user Action to the component action.
046 * <p>
047 * The target manager maintains a reference to a current
048 * target and a target list.
049 * The target list is managed using the <code>addTarget</code> and
050 * <code>removeTarget</code> methods. The current target is managed using the
051 * <code>setTarget</code> and <code>getTarget</code> methods.
052 * <p>
053 * Commands are dispatched to the Targetable objects in the <code>doCommand</code>
054 * method in a well defined order. The doCommand method on the Targetable object
055 * is called and if it returns true then the command has been handled and
056 * command dispatching will stop. If the Targetable doCommand method returns
057 * false then the
058 * <p>
059 * If none of the Targetable objects can handle the command then the default
060 * behaviour is to retrieve an Action from the {@link javax.swing.ActionMap} of
061 * the permanent focus owner with a key that matches the command key. If an
062 * Action can be found then the <code>actionPerformed</code>
063 * method is invoked using an <code>ActionEvent</code> that was constructed
064 * using the command string.
065 * <p>
066 * If the Action is not found on the focus order then the ActionMaps of the ancestor
067 * hierarchy of the focus owner is searched until a matching Action can be found.
068 *  Finally, if none
069 * of the components can handle the command then it is dispatched to the ActionMap
070 * of the current Application instance.
071 * <p>
072 * The order of command dispatch is as follows:
073 * <ul>
074 *    <li>Current Targetable object invoking doCommand method
075 *    <li>List order of Targetable objects invoking doCommand method
076 *    <li>ActionMap entry of the permanent focus owner invoking actionPerfomed
077 *    <li>ActionMap entry of the ancestor hierarchy of the permanent focus owner
078 *    <li>ActionMap entry of the current Application instance
079 * </ul>
080 *
081 * @see Targetable
082 * @see TargetableAction
083 * @author Mark Davidson
084 */
085public class TargetManager {
086
087    private static TargetManager INSTANCE;
088    private List<Targetable> targetList;
089    private Targetable target;
090    private PropertyChangeSupport propertySupport;
091
092    /**
093     * Create a target manager. Use this constructor if the application
094     * may support many target managers. Otherwise, using the getInstance method
095     * will return a singleton.
096     */
097    public TargetManager() {
098        propertySupport = new PropertyChangeSupport(this);
099    }
100
101    /**
102     * Return the singleton instance.
103     */
104    public static TargetManager getInstance() {
105        if (INSTANCE == null) {
106            INSTANCE = new TargetManager();
107        }
108        return INSTANCE;
109    }
110
111    /**
112     * Add a target to the target list. Will be appended
113     * to the list by default. If the prepend flag is true then
114     * the target will be added at the head of the list.
115     * <p>
116     * Targets added to the head of the list will will be the first
117     * to handle the command.
118     *
119     * @param target the targeted object to add
120     * @param prepend if true add at the head of the list; false append
121     */
122    public void addTarget(Targetable target, boolean prepend) {
123        if (targetList == null) {
124            targetList = new ArrayList<Targetable>();
125        }
126        if (prepend) {
127            targetList.add(0, target);
128        } else {
129            targetList.add(target);
130        }
131        // Should add focus listener to the component.
132    }
133
134    /**
135     * Appends the target to the target list.
136     * @param target the targeted object to add
137     */
138    public void addTarget(Targetable target) {
139        addTarget(target, false);
140    }
141
142    /**
143     * Remove the target from the list
144     */
145    public void removeTarget(Targetable target) {
146        if (targetList != null) {
147            targetList.remove(target);
148        }
149    }
150
151    /**
152     * Returns an array of managed targets that were added with the
153     * <code>addTarget</code> methods.
154     *
155     * @return all the <code>Targetable</code> added or an empty array if no
156     *         targets have been added
157     */
158    public Targetable[] getTargets() {
159        Targetable[] targets;
160        if (targetList == null) {
161            targets = new Targetable[0];
162        } else {
163            targets = new Targetable[targetList.size()];
164            targets = (Targetable[])targetList.toArray(new Targetable[targetList.size()]);
165        }
166        return targets;
167    }
168
169    /**
170     * Gets the current targetable component. May or may not
171     * in the target list. If the current target is null then
172     * the the current targetable component will be the first one
173     * in the target list which can execute the command.
174     *
175     * This is a bound property and will fire a property change event
176     * if the value changes.
177     *
178     * @param newTarget the current targetable component to set or null if
179     *       the TargetManager shouldn't have a current targetable component.
180     */
181    public void setTarget(Targetable newTarget) {
182        Targetable oldTarget = target;
183        if (oldTarget != newTarget) {
184            target = newTarget;
185            propertySupport.firePropertyChange("target", oldTarget, newTarget);
186        }
187    }
188
189    /**
190     * Return the current targetable component. The curent targetable component
191     * is the first place where commands will be dispatched.
192     *
193     * @return the current targetable component or null
194     */
195    public Targetable getTarget() {
196        return target;
197    }
198
199    public void addPropertyChangeListener(PropertyChangeListener listener) {
200        propertySupport.addPropertyChangeListener(listener);
201    }
202
203    public void removePropertyChangeListener(PropertyChangeListener listener) {
204        propertySupport.removePropertyChangeListener(listener);
205    }
206
207    /**
208     * Executes the command on the current targetable component.
209     * If there isn't current targetable component then the list
210     * of targetable components are searched and the first component
211     * which can execute the command. If none of the targetable
212     * components handle the command then the ActionMaps of the
213     * focused components are searched.
214     *
215     * @param command the key of the command
216     * @param value the value of the command; depends on context
217     * @return true if the command has been handled otherwise false
218     */
219    public boolean doCommand(Object command, Object value) {
220        // Try to invoked the explicit target.
221        if (target != null) {
222            if (target.hasCommand(command) && target.doCommand(command, value)) {
223                return true;
224            }
225        }
226
227        // The target list has the next chance to handle the command.
228        if (targetList != null) {
229            Iterator<Targetable> iter = targetList.iterator();
230            while (iter.hasNext()) {
231                Targetable target = iter.next();
232                if (target.hasCommand(command) &&
233                    target.doCommand(command, value)) {
234                    return true;
235                }
236            }
237        }
238
239        ActionEvent evt = null;
240        if (value instanceof ActionEvent) {
241            evt = (ActionEvent)value;
242        }
243
244        // Fall back behavior. Get the component which has focus and search the
245        // ActionMaps in the containment hierarchy for matching action.
246        Component comp = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
247        while (comp != null) {
248            if (comp instanceof JComponent) {
249                ActionMap map = ((JComponent)comp).getActionMap();
250                Action action = map.get(command);
251                if (action != null) {
252                    if (evt == null) {
253                        evt = new ActionEvent(comp, 0, command.toString());
254                    }
255                    action.actionPerformed(evt);
256
257                    return true;
258                }
259            }
260            comp = comp.getParent();
261        }
262
263        return false;
264    }
265
266    /**
267     * Resets the TargetManager.
268     * This method is package private and for testing purposes only.
269     */
270    void reset() {
271        if (targetList != null) {
272            targetList.clear();
273            targetList = null;
274        }
275        target = null;
276
277        PropertyChangeListener[] listeners = propertySupport.getPropertyChangeListeners();
278        for (int i = 0; i < listeners.length; i++) {
279            propertySupport.removePropertyChangeListener(listeners[i]);
280        }
281        INSTANCE = null;
282    }
283
284}