/* * Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. Use is * subject to license terms. */ package org.jdesktop.application; import java.awt.KeyboardFocusManager; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.WeakHashMap; import java.util.logging.Logger; import javax.swing.ActionMap; import javax.swing.JComponent; /** * The application's {@code ActionManager} provides read-only cached * access to {@code ActionMaps} that contain one entry for each method * marked with the {@code @Action} annotation in a class. * * * @see ApplicationContext#getActionMap(Object) * @see ApplicationActionMap * @see ApplicationAction * @author Hans Muller (Hans.Muller@Sun.COM) */ public class ActionManager extends AbstractBean { private static final Logger logger = Logger.getLogger(ActionManager.class.getName()); private final ApplicationContext context; private final WeakHashMap<Object, WeakReference<ApplicationActionMap>> actionMaps; private ApplicationActionMap globalActionMap = null; protected ActionManager(ApplicationContext context) { if (context == null) { throw new IllegalArgumentException("null context"); } this.context = context; actionMaps = new WeakHashMap<Object, WeakReference<ApplicationActionMap>>(); } protected final ApplicationContext getContext() { return context; } private ApplicationActionMap createActionMapChain( Class startClass, Class stopClass, Object actionsObject, ResourceMap resourceMap) { // All of the classes from stopClass to startClass, inclusive. List<Class> classes = new ArrayList<Class>(); for(Class c = startClass; ; c = c.getSuperclass()) { classes.add(c); if (c.equals(stopClass)) { break; } } Collections.reverse(classes); // Create the ActionMap chain, one per class ApplicationContext ctx = getContext(); ApplicationActionMap parent = null; for(Class cls : classes) { ApplicationActionMap appAM = new ApplicationActionMap(ctx, cls, actionsObject, resourceMap); appAM.setParent(parent); parent = appAM; } return parent; } /** * The {@code ActionMap} chain for the entire {@code Application}. * <p> * Returns an {@code ActionMap} with the {@code @Actions} defined * in the application's {@code Application} subclass, i.e. the * the value of: * <pre> * ApplicationContext.getInstance().getApplicationClass() * </pre> * The remainder of the chain contains one {@code ActionMap} * for each superclass, up to {@code Application.class}. The * {@code ActionMap.get()} method searches the entire chain, so * logically, the {@code ActionMap} that this method returns contains * all of the application-global actions. * <p> * The value returned by this method is cached. * * @return the {@code ActionMap} chain for the entire {@code Application}. * @see #getActionMap(Class, Object) * @see ApplicationContext#getActionMap() * @see ApplicationContext#getActionMap(Class, Object) * @see ApplicationContext#getActionMap(Object) */ public ApplicationActionMap getActionMap() { if (globalActionMap == null) { ApplicationContext ctx = getContext(); Object appObject = ctx.getApplication(); Class appClass = ctx.getApplicationClass(); ResourceMap resourceMap = ctx.getResourceMap(); globalActionMap = createActionMapChain(appClass, Application.class, appObject, resourceMap); initProxyActionSupport(); // lazy initialization } return globalActionMap; } private void initProxyActionSupport() { KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); kfm.addPropertyChangeListener(new KeyboardFocusPCL()); } /** * Returns the {@code ApplicationActionMap} chain for the specified * actions class and target object. * <p> * The specified class can contain methods marked with * the {@code @Action} annotation. Each one will be turned * into an {@link ApplicationAction ApplicationAction} object * and all of them will be added to a single * {@link ApplicationActionMap ApplicationActionMap}. All of the * {@code ApplicationActions} invoke their {@code actionPerformed} * method on the specified {@code actionsObject}. * The parent of the returned {@code ActionMap} is the global * {@code ActionMap} that contains the {@code @Actions} defined * in this application's {@code Application} subclass. * * <p> * To bind an {@code @Action} to a Swing component, one specifies * the {@code @Action's} name in an expression like this: * <pre> * ApplicationContext ctx = Application.getInstance(MyApplication.class).getContext(); * MyActions myActions = new MyActions(); * myComponent.setAction(ac.getActionMap(myActions).get("myAction")); * </pre> * * <p> * The value returned by this method is cached. The lifetime of * the cached entry will be the same as the lifetime of the {@code * actionsObject} and the {@code ApplicationActionMap} and {@code * ApplicationActions} that refer to it. In other words, if you * drop all references to the {@code actionsObject}, including * its {@code ApplicationActions} and their {@code * ApplicationActionMaps}, then the cached {@code ActionMap} entry * will be cleared. * * @see #getActionMap() * @see ApplicationContext#getActionMap() * @see ApplicationContext#getActionMap(Class, Object) * @see ApplicationContext#getActionMap(Object) * @return the {@code ApplicationActionMap} for {@code actionsClass} and {@code actionsObject} */ public ApplicationActionMap getActionMap(Class actionsClass, Object actionsObject) { if (actionsClass == null) { throw new IllegalArgumentException("null actionsClass"); } if (actionsObject == null) { throw new IllegalArgumentException("null actionsObject"); } if (!actionsClass.isAssignableFrom(actionsObject.getClass())) { throw new IllegalArgumentException("actionsObject not instanceof actionsClass"); } synchronized(actionMaps) { WeakReference<ApplicationActionMap> ref = actionMaps.get(actionsObject); ApplicationActionMap classActionMap = (ref != null) ? ref.get() : null; if ((classActionMap == null) || (classActionMap.getActionsClass() != actionsClass)) { ApplicationContext ctx = getContext(); Class actionsObjectClass = actionsObject.getClass(); ResourceMap resourceMap = ctx.getResourceMap(actionsObjectClass, actionsClass); classActionMap = createActionMapChain(actionsObjectClass, actionsClass, actionsObject, resourceMap); ActionMap lastActionMap = classActionMap; while(lastActionMap.getParent() != null) { lastActionMap = lastActionMap.getParent(); } lastActionMap.setParent(getActionMap()); actionMaps.put(actionsObject, new WeakReference(classActionMap)); } return classActionMap; } } private final class KeyboardFocusPCL implements PropertyChangeListener { private final TextActions textActions; KeyboardFocusPCL() { textActions = new TextActions(getContext()); } public void propertyChange(PropertyChangeEvent e) { if (e.getPropertyName() == "permanentFocusOwner") { JComponent oldOwner = getContext().getFocusOwner(); Object newValue = e.getNewValue(); JComponent newOwner = (newValue instanceof JComponent) ? (JComponent)newValue : null; textActions.updateFocusOwner(oldOwner, newOwner); getContext().setFocusOwner(newOwner); updateAllProxyActions(oldOwner, newOwner); } } } /* For each proxyAction in each ApplicationActionMap, if * the newFocusOwner's ActionMap includes an Action with the same * name then bind the proxyAction to it, otherwise set the proxyAction's * proxyBinding to null. [TBD: synchronize access to actionMaps] */ private void updateAllProxyActions(JComponent oldFocusOwner, JComponent newFocusOwner) { if (newFocusOwner != null) { ActionMap ownerActionMap = newFocusOwner.getActionMap(); if (ownerActionMap != null) { updateProxyActions(getActionMap(), ownerActionMap, newFocusOwner); for (WeakReference<ApplicationActionMap> appAMRef : actionMaps.values()) { ApplicationActionMap appAM = appAMRef.get(); if (appAM == null) { continue; } updateProxyActions(appAM, ownerActionMap, newFocusOwner); } } } } /* For each proxyAction in appAM: if there's an action with the same * name in the focusOwner's ActionMap, then set the proxyAction's proxy * to the matching Action. In other words: calls to the proxyAction * (actionPerformed) will delegate to the matching Action. */ private void updateProxyActions(ApplicationActionMap appAM, ActionMap ownerActionMap, JComponent focusOwner) { for(ApplicationAction proxyAction : appAM.getProxyActions()) { String proxyActionName = proxyAction.getName(); javax.swing.Action proxy = ownerActionMap.get(proxyActionName); if (proxy != null) { proxyAction.setProxy(proxy); proxyAction.setProxySource(focusOwner); } else { proxyAction.setProxy(null); proxyAction.setProxySource(null); } } } }