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 }