001    /*
002     * $Id: TargetManager.java 3271 2009-02-24 19:28:06Z 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    
022    package org.jdesktop.swingx.action;
023    
024    import java.awt.Component;
025    import java.awt.KeyboardFocusManager;
026    import java.awt.event.ActionEvent;
027    import java.beans.PropertyChangeListener;
028    import java.beans.PropertyChangeSupport;
029    import java.util.ArrayList;
030    import java.util.Iterator;
031    import java.util.List;
032    
033    import javax.swing.Action;
034    import javax.swing.ActionMap;
035    import 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     */
085    public 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    }