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 }