/*
* Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. Use is
* subject to license terms.
*/
package org.jdesktop.application;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.ActionMap;
/**
* An {@link javax.swing.ActionMap ActionMap} class where each entry
* corresponds to an <tt>@Action</tt> method from a single
* <tt>actionsClass</tt> (i.e. a class that contains one or more
* <tt>@Actions</tt>). Each entry's key is the <tt>@Action's</tt>
* name (the method name by default), and the value is an
* {@link ApplicationAction} that calls the <tt>@Actions</tt> method.
* For example, the code below prints <tt>"Hello World"</tt>:
* <pre>
* public class HelloWorldActions {
* public @Action void Hello() { System.out.print("Hello "); }
* public @Action void World() { System.out.println("World"); }
* }
* // ...
* ApplicationActionMap appAM = new ApplicationActionMap(SimpleActions.class);
* ActionEvent e = new ActionEvent("no src", ActionEvent.ACTION_PERFORMED, "no cmd");
* appAM.get("Hello").actionPerformed(e);
* appAM.get("World").actionPerformed(e);
* </pre>
*
* <p>
* If a <tt>ResourceMap</tt> is provided then each
* <tt>ApplicationAction's</tt> ({@link javax.swing.Action#putValue
* putValue}, {@link javax.swing.Action#getValue getValue}) properties
* are initialized from the ResourceMap.
*
* <p>
* TBD: explain use of resourcemap including action types, actionsObject,
* actionsClass, ProxyActions,
*
* @see ApplicationAction
* @see ResourceMap
* @author Hans Muller (Hans.Muller@Sun.COM)
*/
public class ApplicationActionMap extends ActionMap {
private final ApplicationContext context;
private final ResourceMap resourceMap;
private final Class actionsClass;
private final Object actionsObject;
private final List<ApplicationAction> proxyActions;
public ApplicationActionMap(ApplicationContext context, Class actionsClass, Object actionsObject, ResourceMap resourceMap) {
if (context == null) {
throw new IllegalArgumentException("null context");
}
if (actionsClass == null) {
throw new IllegalArgumentException("null actionsClass");
}
if (actionsObject == null) {
throw new IllegalArgumentException("null actionsObject");
}
if (!(actionsClass.isInstance(actionsObject))) {
throw new IllegalArgumentException("actionsObject not an instanceof actionsClass");
}
this.context = context;
this.actionsClass = actionsClass;
this.actionsObject = actionsObject;
this.resourceMap = resourceMap;
this.proxyActions = new ArrayList<ApplicationAction>();
addAnnotationActions(resourceMap);
maybeAddActionsPCL();
}
public final ApplicationContext getContext() {
return context;
}
public final Class getActionsClass() {
return actionsClass;
}
public final Object getActionsObject() {
return actionsObject;
}
/**
* All of the {@code @ProxyActions} recursively defined by this
* {@code ApplicationActionMap} and its parent ancestors.
* <p>
* Returns a read-only list of the {@code @ProxyActions} defined
* by this {@code ApplicationActionMap's} {@code actionClass}
* and, recursively, by this {@code ApplicationActionMap's} parent.
* If there are no proxyActions, an empty list is returned.
*
* @return a list of all the proxyActions for this {@code ApplicationActionMap}
*/
public List<ApplicationAction> getProxyActions() {
// TBD: proxyActions that shadow should be merged
ArrayList<ApplicationAction> allProxyActions = new ArrayList<ApplicationAction>(proxyActions);
ActionMap parent = getParent();
while(parent != null) {
if (parent instanceof ApplicationActionMap) {
allProxyActions.addAll(((ApplicationActionMap)parent).proxyActions);
}
parent = parent.getParent();
}
return Collections.unmodifiableList(allProxyActions);
}
private String aString(String s, String emptyValue) {
return (s.length() == 0) ? emptyValue : s;
}
private void putAction(String key, ApplicationAction action) {
if (get(key) != null) {
// TBD log a warning - two actions with the same key
}
put(key, action);
}
/* Add Actions for each actionsClass method with an @Action
* annotation and for the class's @ProxyActions annotation
*/
private void addAnnotationActions(ResourceMap resourceMap) {
Class<?> actionsClass = getActionsClass();
// @Action
for (Method m : actionsClass.getDeclaredMethods()) {
Action action = m.getAnnotation(Action.class);
if (action != null) {
String methodName = m.getName();
String enabledProperty = aString(action.enabledProperty(), null);
String selectedProperty = aString(action.selectedProperty(), null);
String actionName = aString(action.name(), methodName);
Task.BlockingScope block = action.block();
ApplicationAction appAction =
new ApplicationAction(this, resourceMap, actionName, m, enabledProperty, selectedProperty, block);
putAction(actionName, appAction);
}
}
// @ProxyActions
ProxyActions proxyActionsAnnotation = actionsClass.getAnnotation(ProxyActions.class);
if (proxyActionsAnnotation != null) {
for(String actionName : proxyActionsAnnotation.value()) {
ApplicationAction appAction = new ApplicationAction(this, resourceMap, actionName);
appAction.setEnabled(false); // will track the enabled property of the Action it's bound to
putAction(actionName, appAction);
proxyActions.add(appAction);
}
}
}
/* If any of the ApplicationActions need to track an
* enabled or selected property defined in actionsClass, then add our
* PropertyChangeListener. If none of the @Actions in actionClass
* provide an enabledProperty or selectedProperty argument, then
* we don't need to do this.
*/
private void maybeAddActionsPCL() {
boolean needsPCL = false;
Object[] keys = keys();
if (keys != null) {
for (Object key : keys) {
javax.swing.Action value = get(key);
if (value instanceof ApplicationAction) {
ApplicationAction actionAdapter = (ApplicationAction)value;
if ((actionAdapter.getEnabledProperty() != null) ||
(actionAdapter.getSelectedProperty() != null)) {
needsPCL = true;
break;
}
}
}
if (needsPCL) {
try {
Class actionsClass = getActionsClass();
Method m = actionsClass.getMethod("addPropertyChangeListener", PropertyChangeListener.class);
m.invoke(getActionsObject(), new ActionsPCL());
}
catch (Exception e) {
String s = "addPropertyChangeListener undefined " + actionsClass;
throw new Error(s, e);
}
}
}
}
/* When the value of an actionsClass @Action enabledProperty or
* selectedProperty changes, forward the PropertyChangeEvent to
* the ApplicationAction object itself.
*/
private class ActionsPCL implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent event) {
String propertyName = event.getPropertyName();
Object[] keys = keys();
if (keys != null) {
for (Object key : keys) {
javax.swing.Action value = get(key);
if (value instanceof ApplicationAction) {
ApplicationAction appAction = (ApplicationAction)value;
if (propertyName.equals(appAction.getEnabledProperty())) {
appAction.forwardPropertyChangeEvent(event, "enabled");
}
else if (propertyName.equals(appAction.getSelectedProperty())) {
appAction.forwardPropertyChangeEvent(event, "selected");
}
}
}
}
}
}
}