/**
* Copyright 2005 Bushe Enterprises, Inc., Hopkinton, MA, USA, www.bushe.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bushe.swing.action;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import org.xml.sax.SAXException;
import org.bushe.lang.reflect.MethodCallbackInvocationHandler;
import org.bushe.swing.SwingUtils;
/**
* A Manager of Swing Actions
* <p>
* A singleton class used for managing application Actions. Components
* can create and register actions with the ActionManager. Other components
* can grab the actions and customize them by setting callbacks for action event
* handling, enabled state control, group control, toggle control and more.
* <p>
* The ActionXMLReader can read an XML file that describe Actions and register
* them with the ActionManager.
* <p>
* The ActionUIFactory uses the ActionManager's lists to create menus and
* toolbars.
* <p>
* <em>Please see the package documentation to understand the important difference
* between global and local actions.
* <p>
* <h3>Global vs. Local Actions and Action Lists (or getXXX() vs. createXXX()) </h3>
* <p>
* In general usage, there is one global Action instance for each registered id. The
* action instance is returned for each getAction() call and is shared amongst
* every list that referes to the action in every getActionList() result. Similarly,
* a call to getActionList() returns the same global instance, whose Actions it refers
* to are also global.
* <p>
* In same cases, this motif is insufficient. Think about a good editor that allows
* one to view two different files side by side. Each side has a popup menu, but the
* menu choices in the left popup apply to the document on the left and the menu choices
* on the right effect the document on the right. Depending on the type or contents of
* the document the menus may change in enablement, selection, contents, etc. Sharing one
* action list would be inappropriate - adding In this case, the developer may want to
* create two different ActionLists that have different instances of types of Action,
* each pointing to their respective documents (perhaps as the Actions' context). These
* are <em>local</em> Actions and <em>local</em> Action Lists.
* <p>
* Action prototypes, represented by instances of the class ActionAttributes,
* can be registered with the ActionManager. The prototypes are used to create
* actions. Prototypes are always registered when using an XML file.
* Prototypes can be registered when not using an XML file as well. Prototypes
* are sufficient in most cases, and necessary when a global action is
* insuffiecient, i.e. - when a single action id can have more than one
* instance created for it. An example of this usage is a toolbar that belongs
* to a specific component where the component can appear multiple times on the
* screen at once. Each toolbar must run in the context of its component and
* would typically have the component's state, thus necessitating two instances.
* <p>
* <h3>Actions Lists, Action Id Lists, and Instance</h3>
* <p>
* Similarly, Action ID lists serve as prototypes for Action lists. An Action
* Id list contains action ids. A global action list exists for each created
* action id list, and is available from getActionList(Object listId). Action
* id lists only contain ids. Action lists contain actual actions. Components
* such as toolbars and menu bars can be created by passing an action list to
* the ActionUIFactory.
* <p>
* More than one instance of an action list can be created by calling
* createActionList(id). This may be handy if there are two menus who start
* out the same but have their contents change, though this is behavior is
* not managed by the ActionManager or respected by the ActionUIFactory.
* <p>
* Both action id lists and action lists can be multileveled. They may contain
* lists as members. In the case of action id lists, these sub-lists contain
* other action ids (or more sub lists). Action lists contain lists of actions
* or sublists of action (or more sub lists).
* <p>
* Any of the above Lists can be ActionList's. An ActionList is a special List
* that can describe the action (by id) of action that is associated with
* entire list. This allows, for example, an Action to be associated with a
* top-level menu. The Action is fired when menu becomes visible.
* <p>
* Please see package documentation for thorough usage descriptions.
* <p>
* Valid for JDK 1.3 and later.
* <p>
* @author Michael Bushe - owner. Wrote, refactored, converged or re-wrote
* most of it. Blame all issues on him.
* @author Mark Davidson - Sun Swing team member - donated an ActionManager
* included into this one (by reworking it) ex post facto.
* @version 1.0
*/
public class ActionManager implements IconResolver {
/** Key used in Action putValue/getValue to set the id property - must be
* unique within ActionManager instance. Allows two Actions to have the
* same command key (which is OK when they are not used together)*/
public static final String ID = "ID";
/** Key used in putValue/getValue to set the toolbbarShowsText property */
public static final String TOOLBAR_SHOWS_TEXT = "TOOLBAR_SHOWS_TEXT";
/** Key used in putValue/getValue to set the toolbbarShowsText property */
public static final String MENU_SHOWS_ICON = "MENU_SHOWS_ICON";
/** Key used in putValue/getValue to set the type of button that's created
* for the action.*/
public static final String BUTTON_TYPE="BUTTON_TYPE";
/** Value of the buttonType property - allows for the creation of toggle buttons*/
public static final String BUTTON_TYPE_VALUE_TOGGLE = "toggle";
/** Value of the buttonType property - allows for the creation of radio buttons*/
public static final String BUTTON_TYPE_VALUE_RADIO = "radio";
/** Value of the buttonType property - allows for the creation of checkbox buttons*/
public static final String BUTTON_TYPE_VALUE_CHECKBOX = "checkbox";
/** Key used in putValue/getValue to set the group id String of the action.*/
public static final String GROUP = "GROUP";
/** Key used in putValue/getValue to set the icon property - allows
* specification and use of a larger icon than the smicon property*/
public static final String LARGE_ICON = "LARGE_ICON";
/** Key used in putValue/getValue to set the selected property - for toggle
* or group buttons detemines which are selected or unselected*/
public static final String SELECTED = "SELECTED";
/** Key used to store the roles the action applies to.*/
public static final String ACTION_ROLES = "ROLES";
/** Key used in putValue/getValue to set the action class - allows
* for the creation of a specific action type*/
public static final String ACTION_CLASS = "ACTION_CLASS";
/** Key for the Number property used for ordering members of action lists.*/
public static final String WEIGHT = "weight";
private static Class[] ARRAY_OF_ACTION_LISTENER_CLASS = new Class[]{ActionListener.class};
private static Class[] ARRAY_OF_ACTION_EVENT_CLASS = new Class[]{ActionEvent.class};
private static Class[] ARRAY_OF_ITEM_LISTENER_CLASS = new Class[]{ItemListener.class};
private static Class[] ARRAY_OF_ITEM_EVENT_CLASS = new Class[]{ItemEvent.class};
/** The set of prototypes by id.*/
private Map s_prototypes = Collections.synchronizedMap(new HashMap());
/** A keyed set of lists, each list is a mixed-type list of object ids
* or sub-Lists (typically ActionLists), each id points to a registered
* action. The ids on the map can be any object, but Strings are typical.*/
private Map s_actionIdLists = Collections.synchronizedMap(new HashMap());
/** The global set of actions by id.*/
private Map s_globalActions = Collections.synchronizedMap(new HashMap());
/** A keyed set of lists, each list is a mixed-type list of Actions
* or Lists (typically ActionLists). The ids on the map can be any object,
* but Strings are typical.*/
private Map s_globalActionLists = Collections.synchronizedMap(new HashMap());
/** A role names for the user using the manager is managing. Actions will
* only be available for specified roles.*/
private List roles;
/** Resolves icon paths to icons for custom resolution. */
private IconResolver iconResolver;
/** The one default instance.*/
private static ActionManager INSTANCE;
/** Used to hold named instances*/
private static Map NAMED_INSTANCES;
private static Object INSTANCE_LOCK = new Object();
/**
* Cannot instantiate this directly, only because if someone is using the API
* quickly, then they are steered to the commonly used getInstance() method.
* <p>
* However, since more than one ActionManager may be useful in certain situations, the constructor
* is made protected to allow multiple ActionManagers to exist by extension. It's a bit of a pain,
* but it's a bigger pain to expose this publicly when it would rarely be needed.
*/
protected ActionManager() {
}
/**
* Return the instance of the ActionManager if this will be used
* as a singleton. The instance will be created if it hasn't
* previously been set.
*
* @return the ActionManager instance.
* @see #setInstance(String, ActionManager)
*/
public static ActionManager getInstance() {
synchronized (INSTANCE_LOCK) {
if (INSTANCE == null) {
INSTANCE = new ActionManager();
setInstance(null, INSTANCE);
}
}
return INSTANCE;
}
/**
* Add a named static ActionManager instance. Typically not needed, but can be useful
* to separate groups of actions into their own managers.
* <p>
* Calling this method twice with the same name silently replaces the old manager with the new one.
* @param name the name to use in getInstance(String) to get the instance, if null it replaces
* the default ActionManager returned via getInstance().
* @param actionManager the ActionManager to match to the name
*/
public static void setInstance(String name, ActionManager actionManager) {
synchronized (INSTANCE_LOCK) {
if (NAMED_INSTANCES == null) {
NAMED_INSTANCES = Collections.synchronizedMap(new HashMap());
}
NAMED_INSTANCES.put(name, actionManager);
if (name == null) {
INSTANCE = actionManager;
}
}
}
/**
* Add a named static ActionManager instance. Typically not needed, but can be useful
* to separate groups of actions into their own managers.
* <p>
* Calling this method twice with the same name silently replaces the old manager with the new one.
* @param name the name to use in getInstance(String) to get the instance, if null it replaces
* the default ActionManager returned via getInstance().
*/
public static ActionManager createNamedInstance(String name) {
ActionManager actionManager = new ActionManager();
setInstance(name, actionManager);
return actionManager;
}
/**
* Get a named static ActionManager instance.
* @param name the name of the instance as added by setInstance(), if null call is
* same as getInstance();
* @return the named ActionManager instance, null if not found.
*/
public static ActionManager getInstance(String name) {
synchronized (INSTANCE_LOCK) {
if (NAMED_INSTANCES == null) {
if (name == null) {
return getInstance();
} else {
return null;
}
} else {
return (ActionManager)NAMED_INSTANCES.get(name);
}
}
}
/**
* Resets the ActionManager to its created state.
*/
public void reset() {
s_prototypes = Collections.synchronizedMap(new HashMap());
s_actionIdLists = Collections.synchronizedMap(new HashMap());
s_globalActions = Collections.synchronizedMap(new HashMap());
s_globalActionLists = Collections.synchronizedMap(new HashMap());
}
/**
* Set the role names (Strings) the user act as. The Action Manager
* will only return roles with no role declared or with at least one
* role matching declared matching one of the the set List of roles.
* @param roles a List of Strings
*/
public void setRoles(List roles) {
this.roles = roles;
}
/**@return an unmodifiable List of Strings of the role names the user plays.*/
public List getRoles() {
if (roles == null) {
return null;
}
return Collections.unmodifiableList(roles);
}
public synchronized void register(File f) throws IOException, SAXException {
ActionXMLReader actionXMLReader = new ActionXMLReader(this);
actionXMLReader.loadActions(f);
}
/**
* Registers the actions attributes (prototypes), action lists, and
* action sets from an XML file. Same as creating an ActionXMLReader,
* loading action attributes and registering them.
*
* @param actionXMLURL A URL to an action XML file, must not be null.
* @throws IOException on unhandlable conditions such as not opening the file,
* invalid file
* @throws SAXException if there is an XML error, such as an action with the same name is already registered,
* etc.
*/
public synchronized void register(URL actionXMLURL) throws IOException, SAXException {
register(actionXMLURL, false);
}
/**
* Same as registerFromURL(URL), except it allows debug output from the XML
* reader.
* @param actionXMLURL A URL to an action XML file, must not be null.
* @param debug whether or not the reader outputs debug level
*/
public synchronized void register(URL actionXMLURL, boolean debug) throws IOException, SAXException {
ActionXMLReader actionXMLReader = new ActionXMLReader(this);
actionXMLReader.setDebug(debug);
//read in
actionXMLReader.loadActions(actionXMLURL);
}
/**
* Registers the actions attributes (prototypes), action lists, and
* action sets from an XML file. Same as creating an ActionXMLReader,
* loading action attributes and registering them.
*
* @param actionXMLStream A stream to an action XML file, must not be null.
* @throws IOException on unhandlable conditions such as not opening the file,
* invalid file
* @throws SAXException if there is an XML error, such as an action with the same name is already registered,
* etc.
*/
public synchronized void register(InputStream actionXMLStream) throws IOException, SAXException {
register(actionXMLStream, false);
}
/**
* Same as registerFromURL(URL), except it allows debug output from the XML
* reader.
* @param actionXMLStream A stream to an action XML file, must not be null.
* @param debug whether or not the reader outputs debug level
*/
public synchronized void register(InputStream actionXMLStream, boolean debug) throws IOException, SAXException {
ActionXMLReader actionXMLReader = new ActionXMLReader(this);
actionXMLReader.setDebug(debug);
//read in
actionXMLReader.loadActions(actionXMLStream);
}
/**
* Registers a global action with the ActionManager. If a prototype for the
* id exists, it is ignored for future getAction() calls
* (but the prototype is used for createAction() calls).
* @param id the key for the action (usually a String)
* @param action The action to be registered, must not be null.
* @throws IllegalArgumentException if an action with the same name is
* already registered, or if the action param is null.
*/
public synchronized void registerAction(Object id, Action action) throws
IllegalArgumentException {
if (action == null) {
throw new IllegalArgumentException("Null action registered to ActionManager.");
}
if (s_globalActions.containsKey(id)) {
throw new IllegalArgumentException("An action by the name " +
id + "already exists.");
}
if (s_prototypes.containsKey(id)) {
throw new IllegalArgumentException("An prototype action by the name " +
id + "already exists.");
}
s_globalActions.put(id, action);
}
/**
* Deregisters the specified global action from the ActionManager.
* @todo - how to update the list that hold it?
* @param id The id of the action to be deregistered, equal to the id used
* to register it.
* @return true if an action with the id was removed. If not
* found, false is returned.
*/
public synchronized boolean deregisterAction(Object id) {
Object o = s_globalActions.remove(id);
return o != null;
}
/**
* Registers an action protoype with the ActionManager. An action prototype
* is a set of attributes that the ActionManager can use to create and
* Action when getAction(), createAction(), or getNewAction() are called
* (directly or for action lists).
* @param id The id of the action to be registered, must not be null.
* @param actionAtts the prototype data
* @throws IllegalArgumentException if an action with the same name is
* already registered, or if the action param is null.
*/
public synchronized void registerActionPrototype(Object id, ActionAttributes actionAtts) throws
IllegalArgumentException {
if (actionAtts == null) {
throw new IllegalArgumentException("Null action registered to ActionManager.");
}
if (s_prototypes.containsKey(id)) {
throw new IllegalArgumentException("An action protoype by the name " +
id + "already exists.");
}
if (s_globalActions.containsKey(id)) {
throw new IllegalArgumentException("An action by the name " +
id + "already exists.");
}
s_prototypes.put(id, actionAtts);
}
/**
* Gets all the action ids available for use. Includes all the registered
* global actions and the ids of action prototypes that have not been
* created globally. Does not filters out actions based on roles.
* @return A Set of all the actionIds.
*/
public Set getActionIds() {
HashSet actionIds = new HashSet();
actionIds.addAll(s_globalActions.keySet());
actionIds.addAll(s_prototypes.keySet());
return actionIds;
}
/**
* Gets the action ids for all registered global actions. Does not include
* ids of action prototypes that have not been created globally.
* @return A Set of all the actionIds
*/
public Set getGlobalActionIds() {
return s_globalActions.keySet();
}
/**
* Gets the action ids for all registered action prototypes. Does not
* include the ids of global actions registered directly (not created via
* a prototype)
* @return A Set of all the actionIds
*/
public Set getPrototypeActionIds() {
return s_prototypes.keySet();
}
/**
* @param id Object an id of a registered prototype
* @return ActionAttributes the attrbutes of that prototype
*/
public ActionAttributes getPrototype(Object id) {
return (ActionAttributes)s_prototypes.get(id);
}
/**
* Gets the registered Action by action id. If the action has not been
* registered, then an Action is created from the prototype (an
* ActionAttributes instance) matching the id, and registered as a
* global action.
* <p>
* Filters out actions based on roles. If the action doesn't define
* a role, it is returned, if it does define one or more roles, then
* the it is returned only if at least one of this manager's roles match
* one of the roles defined by the action.
* @param actionId The action id for the registered Action
* @return The global Action instance for this id or null if the id
* is not registered as an Action or a prototype.
*/
public Action getAction(Object actionId) {
Action action = (Action) s_globalActions.get(actionId);
if (action == null) {
action = createAction(actionId);
if (action != null) {
s_globalActions.put(actionId, action);
}
}
if (action != null) {
List list = (List)action.getValue(ACTION_ROLES);
if (list != null && !containsAny(roles, list)) {
return null;
}
}
return action;
}
/**
* Same as createAction(prototypeId, null).
* @param prototypeId The action id for the registered prototype
* @return A new Action instance for this id or null if the id
* is not registered as a prototype.
*/
public Action createAction(Object prototypeId) {
return createAction(prototypeId, null);
}
/**
* Creates a new instance of Action given an id of a registered prototype.
* Does not register the action with the ActionManager. If the prototype
* has not been registered, then null is returned.
* <p>
* If the action to be created has roles defined, and at least one of the
* ActionManager's roles don't match one of the action's role, then null is
* returned.
* <p>
* Uses ActionAttributes to create the action. For more information on
* how to set the default action class or how actions are created, see
* {@link ActionAttributes}
* <p>
* See class and package documentation for usage description.
* @param prototypeId The action id for the registered prototype
* @param context If the action is {@link ContextAware}, then the set of
* name-value piars is set on it.
* @return A new Action instance for this id or null if the id
* is not registered as a prototype or the current roles are no appropriate
*/
public Action createAction(Object prototypeId, Map context) {
ActionAttributes attrs = (ActionAttributes) s_prototypes.get(prototypeId);
if (attrs == null) {
return null;
}
Action action = attrs.createAction();
if (!passesRoleTest(action)) {
return null;
}
if (action instanceof ContextAware) {
ContextAware caa = (ContextAware) action;
caa.setContext(context);
}
return action;
}
/**
* @param action the action to test roles against, if null, false returned
* @return true if the action has no roles or has roles and has one of the
* roles specified in this ActionManager*/
protected final boolean passesRoleTest(Action action) {
if (action == null) {
return false;
}
return passesRoleTest((List)action.getValue(ActionManager.ACTION_ROLES));
}
/**@return true if the actionList has no roles or has roles and has one of
* the roles specified in this ActionManager*/
protected final boolean passesRoleTest(ActionList actionList) {
return passesRoleTest(actionList.getRoles());
}
/**@return true if the List of defined roles is null or has one of the
* roles specified in this ActionManager*/
private boolean passesRoleTest(List defRoles) {
if (defRoles == null) {
return true;
}
return containsAny(roles, defRoles);
}
/**@return true if any of the target collection contains any elements in
* source collection */
public boolean containsAny(Collection src, Collection target) {
if (src == null) {
return false;
}
Iterator iter = src.iterator();
while (iter.hasNext()) {
if (target.contains(iter.next())) {
return true;
}
}
return false;
}
/**
* An action id list is a list containing action ids. The list can be
* multileveled by containing other Lists (typically ActionLists containing
* ids). You can register a list with the ActionManager, and then later
* get a toolbar or menu created matching the list by passing the list id
* (typically a name) to the ActionUIFactory.
* @param actionList a hierarchical list of actions, ActionLists, separators or
* or null (for separators).
* @throws IllegalArgumentException if the listhas already been registered.
*/
public synchronized void registerActionList(ActionList actionList) throws IllegalArgumentException {
if (s_globalActionLists.get(actionList.getId()) != null) {
throw new IllegalArgumentException("Already registered action list named:" + actionList.getId());
}
s_globalActionLists.put(actionList.getId(), actionList);
}
/**
* An action id list is a list containing action ids. The list can be
* multileveled by containing other Lists (typically ActionLists containing
* ids). You can register a list with the ActionManager, and then later
* get a toolbar or menu created matching the list by passing the list id
* (typically a name) to the ActionUIFactory.
* @param actionIds the ids of the actions in the list, in order, use
* "SEPARATOR" or null to add a separator.
* @throws IllegalArgumentException if the listhas already been registered.
*/
public synchronized void registerActionIdList(ActionList actionIds) throws
IllegalArgumentException {
if (s_actionIdLists.get(actionIds.getId()) != null) {
throw new IllegalArgumentException("Already registered action id list named:" + actionIds.getId());
}
s_actionIdLists.put(actionIds.getId(), actionIds);
}
/**
* Gets the action ids for all registered actions.
* <p>Not filtered by role
* @return A Set of all the ids for all registered action lists
*/
public Set getActionListIds() {
return s_actionIdLists.keySet();
}
/**
* Get the List of ids registered to the ActionManager for the
* given list id for which they were registered (in order). Typically an
* ActionList.
* <p>
* The list contains ids for actions, not the actions themselves.
* <p>
* The result may be multileveled. It may contain other id Lists
* (or ActionLists), which in turn contain other ids or ActionLists.
* <p>
* The ActionList is returned only if it has no roles defined or it has
* roles and has at least one of the ActionManager's roles. Each element
* of the List is filtered according to the element's roles as well.
*
* @param listId The id of the list as set when registered
* @return the List as registered via registerActionIdList for the given
* list id. Null if no list was registered with that id. The list is
* the original lists, it has ids, not Actions. Id lists with roles
* defined that don't match at least one role of the ActionManager are
* not returned (null is returned)
*/
public synchronized ActionList getActionIdList(Object listId) {
ActionList idList = (ActionList) s_actionIdLists.get(listId);
if (idList == null || !passesRoleTest(idList)) {
return null;
}
return idList;
}
/**
* Deregisters the specified action list from the ActionManager.
* @param listId The id of the action id list to be deregistered, equal to
* the key used when it was registered.
* @return true if an action list with the actionId was removed. If not
* found, false is returned.
*/
public synchronized boolean deregisterIdList(Object listId) {
Object o = s_actionIdLists.remove(listId);
s_globalActionLists.remove(listId);
return o != null;
}
/**
* Gets or creates the Global ActionList with their Global Actions.
*
* Use this method instead of {@link #createActionList(Object)} if
* the action list will only ever be created once (like a static
* toolbar). The list of actions is cached the first time it is created.
* <p>
* The Actions in the list are the same action instances as the global
* actions available via getAction(id).
* <p>
* The ActionList is returned only if it has no roles defined or it has
* roles and has at least one of the ActionManager's roles. Each element
* of the List is filtered according to the element's roles as well.
*
* @param listId The id of the list as set when registered
* @return the List of Actions for this list id. Created on first call
* for the id and cached thereafter. Null if no list was registered with
* that id. The list made via a call to createActionList, so it
* contains Actions (and sub-Lists and nulls), rather than ids.
*/
public synchronized ActionList getActionList(Object listId) {
ActionList result = (ActionList) s_globalActionLists.get(listId);
if (result == null) {
result = createActionList(listId, true, null);
if (result == null) {
return null;
} else {
s_globalActionLists.put(listId, result);
return result;
}
} else {
if (!passesRoleTest(result)) {
return null;
} else {
return result;
}
}
}
/**
* Creates a new Local ActionList with Global Actions specified by the
* action list id.
* <p>
* Same as createActionList(actionListId, true, null)}
* @param actionListId the id of the list as registered with registerActionIdList
* @return an ActionList of Actions, Lists and nulls, sublists can have Actions,
* Lists, ActionLists, and nulls. All Actions are references to the global
* Actions available via getAction(id).
*/
public synchronized ActionList createActionList(Object actionListId) {
return createActionList(actionListId, true, null);
}
/**
* Creates a new Local ActionList with a context and Global Actions as
* specified by the action list id.
* <p>
* Same as createActionList(Object, true, Object)}
* @param actionListId the id of the list as registered with registerActionIdList
* @param context If the action is {@link ContextAware}, then the set of
* name-value piars is set on it.
* @return an ActionList of Actions, Lists and nulls, sublists can have Actions,
* Lists, ActionLists, and nulls. All Actions are references to the global
* Actions available via getAction(id).
*/
public synchronized ActionList createActionList(Object actionListId, Map context) {
return createActionList(actionListId, true, context);
}
/**
* Creates a new Local ActionList with a context and either new
* Local Actions or Global Actions.
* <p>
* Same as createActionList(Object, boolean, null)}
* @param actionListId the id of the list as registered with registerActionIdList
* @param useGlobalActions true if the list should be filled with references
* to the global actions (getAction()), false is new action lists should
* be created from the prototype available via createAction().
* @return an ActionList of Actions, Lists and nulls, sublists can have Actions,
* Lists, ActionLists, and nulls. All Actions are references to the global
* Actions available via getAction(id).
*/
public synchronized ActionList createActionList(Object actionListId, boolean useGlobalActions) {
return createActionList(actionListId, useGlobalActions, null);
}
/**
* Turns Action Id Lists into a List of Actions.
* <p>
* Creates a new List of Actions from the id of the action
* list registered with the Action Manager.
* <p>
* The instances of the Actions in the newly created. They are not the
* same instances in other list and must be managed
* <p>
* The List is multileveled - in addition to Actions, it may contain Lists
* that contain Actions. It may also contain null, which should be
* interpreted as a separator. Sublists may also contain Lists and null.
* If the Lists or sublists are ActionLists, then the result uses
* ActionLists for those lists as well.
* <p>
* The ActionList is returned only if it has no roles defined or it has
* roles and has at least one of the ActionManager's roles. Each element
* of the List is filtered according to the element's roles as well.
*
* @param actionListId the id of the list as registered with registerActionIdList
* @param useGlobalActions true if the list should be filled with references
* to the global actions (getAction()), false is new action lists should
* be created from the prototype available via createAction().
* @param context If the action is {@link ContextAware}, then the set of
* name-value piars is set on it.
* @return An ActionList of Actions, Lists and nulls, sublists can have
* Actions, Lists, ActionLists, and nulls.
* @see #getActionList(Object) for an alternative
*/
public synchronized ActionList createActionList(Object actionListId,
boolean useGlobalActions, Map context) {
ActionList actionIdList = getActionIdList(actionListId);
if (actionIdList == null) {
return null;
}
ActionList actionList = new ActionList(actionIdList.getId(),
actionIdList.getTriggerActionId());
actionList.setWeight(actionIdList.getWeight());
fillListWithActions(actionList, actionIdList, useGlobalActions, context);
return actionList;
}
/**
* Rercusive method for turning action id lists into Action Lists
* @param toFill the ActionList to fill with ACtions
* @param actionIds the list of action ids to use in filling it
* @param useGlobalActions true if the list should be filled with references
* @param context If the action is {@link ContextAware}, then the set of
* name-value pairs is set on it.
* implement ContextAware, only if useGlobalActions is false.
* to the global actions (getAction()), false is new action lists should
* be created from the prototype available via createAction().
*/
private synchronized void fillListWithActions(List toFill, List actionIds,
boolean useGlobalActions,
Map context) {
for (int i = 0; i < actionIds.size(); i++) {
Object elem = actionIds.get(i);
Number weight = null;
if (elem == null) {
toFill.add(null);
} else if (elem instanceof Separator) {
weight = ((Separator)elem).getWeight();
insertWithRespectToWeights(weight, toFill, elem);
} else if (elem instanceof List) {
//use recursion for sublist
List subIds = (List) elem;
List subList = null;
if (subIds instanceof ActionList) {
ActionList subAL = (ActionList) subIds;
if (!passesRoleTest((ActionList)subAL)) {
continue;
}
subList = new ActionList(subAL.getId(),
subAL.getTriggerActionId());
weight = subAL.getWeight();
((ActionList)subList).setWeight(weight);
} else {
subList = new ArrayList(subIds.size());
}
insertWithRespectToWeights(weight, toFill, subList);
fillListWithActions(subList, subIds, useGlobalActions, context);
} else {
//get the action for the id and add it to the list
Action action = null;
if (useGlobalActions) {
action = getAction(actionIds.get(i));
} else {
action = createAction(actionIds.get(i), context);
}
if (action != null) {
Object weightVal = action.getValue(ActionManager.WEIGHT);
if (weightVal != null) {
weight = Double.valueOf(weightVal+"");
}
insertWithRespectToWeights(weight, toFill, action);
}
}
}
}
/**
* @param weight Number:, not null
* @param toFill List to find insertion point
*/
private void insertWithRespectToWeights(Number weight, List toFill, Object toInsert) throws NumberFormatException {
for (int i = 0; i < toFill.size(); i++) {
Number childWeight = null;
Object child = toFill.get(i);
if (child instanceof ActionList) {
childWeight = ((ActionList)child).getWeight();
} else if (child instanceof Action) {
Object val = ((Action)child).getValue(ActionManager.WEIGHT);
if (val != null) {
childWeight = Double.valueOf(val + "");
}
} else if (child instanceof Separator) {
Object val = ((Separator)child).getWeight();
if (val != null) {
childWeight = Double.valueOf(val + "");
}
}
if (childWeight == null) {
//Treat child weight as 0
if (weight != null && weight.doubleValue() < 0.0d) {
//This is the weight == -1
toFill.add(i, toInsert);
return;
}
} else {
//Treat weight as 0
if (weight == null) {
if (childWeight.doubleValue() > 0.0d) {
toFill.add(i, toInsert);
return;
}
} else if (weight.doubleValue() < childWeight.doubleValue()) {
toFill.add(i, toInsert);
return;
}
}
}
toFill.add(toInsert);
}
/**
* Registers a callback method called when the Global Action corresponding
* to the action id gets invoked.
* <p>
* Handy method to specify a method to call on an object when and action
* event is fired without having to implement ActionListener (while still
* getting the ActionEvent if necessary). The typical use of this method
* is a controller that listens to multiple actionas and wants a method
* to be called for each (while avoiding large "if" dispatching
* structures in an actionPerformed()).
* <p>
* If the method of the handler class identified by the "method" parameter
* takes an ActionEvent as a parameter, then the
* ActionEvent for the action is passed to the method, otherwise, it must
* be a no-arg method.
* <p>
* @todo Overload to use the javax.beans.EventHandler-type string to handle
* parameters and even implementation, i.e. - ..., "source.name", "text"
* calls setText on the hanlder with the event source's getName() result.
* <p>
* If handler is a Class, then the action is called on the static method of
* that class named by method. If the handler is not a class, then the
* method can be static or non-static (instance).
* <p>
* If the method is overloaded (static or instance) then the signature that
* defines the ActionEvent takes precedence and will be called, the others
* will be ignored.
* <p>
* Normal rules for Java Security, Classloading, and Reflection apply.
* <p>
* @param actionId value of the action id
* @param callback the object which will perform the action, if a Class,
* then method must be a static method of the class
* @param method the name of the method on the handler which will be called.
* @throws IllegalArgumentException if any of the parameters except actionId
* are null (an actionId of null - the null action - is conceivable), or if
* getAction(actionId) returns null, or if the Action is not a
* ActionListenerDelegator
* @throws NoSuchMethodException if the method is not found in the class, or
* if the method is non-static, but the callback is an instance of Class.
*/
public void registerActionCallback(Object actionId, Object callback, String method) throws NoSuchMethodException {
Action action = getAction(actionId);
if (action == null) {
throw new IllegalArgumentException("No action found for action id " + actionId
+ ", registering method " + method + ", callback "
+ callback);
}
if (action instanceof Actionable) {
registerActionCallback((Actionable)action, callback, method) ;
} else {
throw new NoSuchMethodException("Action does not implement Actionable, no addActionListener(0 to use: action " + action
+ ", registering method " + method + ", callback "
+ callback);
}
}
/**
* Same as registerActionCallback(Object actionId, Object callback,
* String method), except it takes the Action instead of the action id
* (for use with non-global actions).
*
* @param action the action to which a dynamic listener will be added
* @param callback the object which will perform the action, if a Class,
* then method must be a static method of the class
* @param method the name of the method on the handler which will be called.
* @throws NoSuchMethodException if the method is not found, or not static
* if it should be (if callback is a Class)
* @throws IllegalArgumentException if any of the parameters are null,
* if the Action is not a ActionListenerDelegator
* @todo unregister
*/
public void registerActionCallback(Actionable action, Object callback, String method) throws NoSuchMethodException {
ActionListener listenerProxy = (ActionListener) registerCallback(action, callback, method,
ARRAY_OF_ACTION_EVENT_CLASS, ActionListener.class, ARRAY_OF_ACTION_LISTENER_CLASS);
action.addActionListener(listenerProxy);
}
/**
* Same as registerCallback(Object actionId, Object callback, String method)},
* except adds type saftey for static methods.
* @param actionId the id of the global action
* @param callbackClass the Class whose's static method willbe called
* @param staticMethod the name of the class's staticMethod
*/
public void registerStaticActionCallback(Object actionId, Class callbackClass, String staticMethod) throws NoSuchMethodException{
registerActionCallback(actionId, callbackClass, staticMethod);
}
/**
* Handles all the callback registrations
* @param action the id of the action
* @param callback the proxy to callback, can be null for static methods
* @param method the method to call, not null
* @param action the action to work on.
* @throws IllegalArgumentException if action, handler or method is null
* @throws SecurityException
* @throws NoSuchMethodException
*/
private Object registerCallback(Actionable action, Object callback, String method, Class[] args,
Class implementingInterface, Class[] arrayOfInterfaces)
throws IllegalArgumentException, SecurityException, NoSuchMethodException {
if (action == null) {
throw new IllegalArgumentException("Action cannot be null.");
}
try {
return MethodCallbackInvocationHandler.createMethodCallbackProxy(callback, method, args,
implementingInterface, arrayOfInterfaces);
} catch (NoSuchMethodException ex) {
String err = "Callback Method named " + method;
if (callback instanceof Class) {
err = err + " either not found or is not static ";
} else {
err = err + " is not found ";
}
err = err + " not found in callback " + callback
+ ", registering for action =" + action;
throw new NoSuchMethodException(err);
}
}
/**
* Calls setEnabled(boolean) on all the created Global Actions .
* Use this actions to enabled or disable all Global Actions.
* @param enable true to enable the action, false to disable it.
*/
public void setEnabledAll(boolean enable) {
if (s_globalActions == null || s_globalActions.isEmpty()) {
return;
}
Iterator iter = s_globalActions.keySet().iterator();
while (iter.hasNext()) {
setEnabled(iter.next(), enable);
}
}
/**
* Calls setEnabled() on the created Global Action corresponding to the
* given id.
* <p>
* The action must have already been created. Use this method to
* enable or disable a Global Action only if it has been created.
* @param enable true to enable the action, false to disable it.
*/
public void setEnabled(Object id, boolean enable) {
if (s_globalActions == null || s_globalActions.isEmpty()) {
return;
}
Action action = (Action) s_globalActions.get(id);
if (action == null) {
return;
}
action.setEnabled(enable);
}
/**
* Calls updateEnabledState() on all the created Global Actions that implement
* {@link EnabledUpdater}. Use this actions to refresh the enabled state of
* all Global Actions, if your global actions know how to check themselves.
*/
public void updateEnabledAll() {
if (s_globalActions == null || s_globalActions.isEmpty()) {
return;
}
Iterator iter = s_globalActions.keySet().iterator();
while (iter.hasNext()) {
updateEnabled(iter.next());
}
}
/**
* Sets a context value for all the created actions if the
* Action is ContextAware (BasicAction is ContextAware).
* Depending on the action, this will force an updateEnabledState()
* (it will by default for any BasicAction).
* @see ContextAware
* @see BasicAction
* @param key context key for actions.
* @param contextValue the context value for actions.
*/
public void putContextValueForAll(Object key, Object contextValue) {
if (s_globalActions == null || s_globalActions.isEmpty()) {
return;
}
Iterator iter = s_globalActions.keySet().iterator();
while (iter.hasNext()) {
Object action = s_globalActions.get(iter.next());
if (action instanceof ContextAware) {
ContextAware contextAwareAction = (ContextAware)action;
contextAwareAction.putContextValue(key, contextValue);
}
}
}
/**
* Calls updateEnabledState() on the Global Actions corresponding to the given id.
* The action must have already been created. Only called if the action
* implements {@link EnabledUpdater}. Use this method to refresh the enabled
* state of a Global Action, if your Global Actions knows how to
* check itself in such a mannner.
*/
public void updateEnabled(Object id) {
if (s_globalActions == null || s_globalActions.isEmpty()) {
return;
}
Action action = (Action) s_globalActions.get(id);
if (action instanceof EnabledUpdater) {
((EnabledUpdater) action).updateEnabled();
}
}
/**
* Utility method to put a mapping in a component's input and action maps
* for an action. Assume
* @param comp JComponent any component that want to
* @param action Action
* @param condition JComponent constant one of
* JComponent.WHEN_IN_FOCUSED_WINDOW
* JComponent.WHEN_FOCUSED
* JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
*/
public static void mapKeystrokeForAction(JComponent comp, Action action, int condition) {
KeyStroke keystroke = (KeyStroke)action.getValue(Action.ACCELERATOR_KEY);
if (keystroke == null) {
return;
}
comp.getActionMap().put(action.getValue(Action.ACTION_COMMAND_KEY), action);
comp.getInputMap(condition).put(keystroke, action.getValue(Action.ACTION_COMMAND_KEY));
}
/**
* Same as mapKeystrokeForAction(JComponent, Action, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT}
* @param comp JComponent any component that want to
* @param action Action to map the keystroke accelrator for
*/
public static void mapKeystrokeForAction(JComponent comp, Action action) {
mapKeystrokeForAction(comp, action, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
}
/**
* @return the icon resolver set on this class, will be "this" if not explictly set
*/
public IconResolver getIconResolver() {
return iconResolver;
}
/**
* @param iconResolver a resolver that can take an imageURL string and return an Icon
*/
public void setIconResolver(IconResolver iconResolver) {
this.iconResolver = iconResolver;
}
public Icon resolveIcon(String imageURL) {
if (iconResolver != null) {
return iconResolver.resolveIcon(imageURL);
} else {
return SwingUtils.getIcon(imageURL);
}
}
}