/** * 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.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import javax.swing.AbstractAction; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.KeyStroke; import javax.swing.event.EventListenerList; /** * A useful base class to use for Actions. See package documentation. * <p> * The Swing Action Manager libray makes no requirements for its use, but if you use * BasicAction or its interfaces itegrating with the Action Framework is a lot easier. * <p> * Typical usage of BasicAction is to extend and override execute(), or * add a an ActionListener to the action. To handle enablement, either * shouldBeEnabled() is overriden or an EnabledUpdater delegate is added to the * action. * <p> * BasicAction implements: * <ul> * <li>{@link Actionable} so that actionPerformed can be delegated, typically to a * controller * <li>{@link ItemAction} so the framework (specifically ActionUIFactory) can keep * multiple JCheckBoxMenuItems (and similar) checked when they share the * same action. Typically applications will not need to use ItemAction or * ItemEvents, use ActionEvents instead (i.e. override execute() or call * addActionListener()). To find out whether an action is selected, call * BasicAction's isSelected() method. * <li>{@link EnabledUpdater} to allow an application controller to tell the action * to set itself as enabled or disabled, typically by calling it's enabled * delegates or checking it's context. * <li>{@link DelegatesEnabled} to allow the action to defer to a delegate (typically an * application controller) when it computes whether it should be enabled or * disabled. * <li>{@link ContextAware} to allow a context to be set on an action, the context can * contain anything the action needs to do its work - data, active components, * etc. By default, the action calls updateEnabledState() when the context changes. * </ul> * <p> * It is recommended that BasicAction be extended for all an application's * actions, particularly to improve exception handling. An easy way to * accomplish this is to create the class and set the defaultActionClass * attribute on the action-set element to your applications derivative of * BasicAction. * @see Package Documentation for a complete description * @author Michael Bushe */ public class BasicAction extends AbstractAction implements Actionable, ItemAction, DelegatesEnabled, ContextAware { /** The value returned by shouldBeEnabled() by default.*/ protected static boolean DEFAULT_ENABLED_STATE = true; //Used for multiple event types - actions, events, for the action EventListenerList listenerList = new EventListenerList(); /** If set, calls to shouldBeEnabled are delegated to the list.*/ private List enabledDelegates; /** A map of context values that can be set by the user and used for any purpose. * Most typically it would be set to a model the component would use * or a component or window that the action must run in the context of.*/ private Map context; /** JUST FOR DEBUGGING PURPOSES - so the action is easily identifiable * in a watch list */ private String idForDebugging = null; /** * Default Constructor, nice for use with XML. */ public BasicAction() { } /** * Simple name-only constructor * @param id the action id, name, command name (button name) * @see AbstractAction */ public BasicAction(String id) { this(id, null); } /** * Simple id and icon constructor * @param id the id, name, and command name of the action * @param icon the icon used in the actoin's toolbar button or menu items * @see javax.swing.AbstractAction */ public BasicAction(String id, Icon icon) { this(id, null, null, null, null, icon); } /** * Commonlu used constructor * @param id the id, name, and command name of the action * @param mnemonic the action's mnemonic - for menu traversal * @param accelerator the action's accelerator - "quick key", no menu * @param icon the icon used in the actoin's toolbar button or menu items * @see javax.swing.AbstractAction */ public BasicAction(String id, Integer mnemonic, KeyStroke accelerator, Icon icon) { this(id, null, null, mnemonic, accelerator, icon); } /** * Same as nearly Full Constructor, but the id, name and command name are * all set to the id. * @param id the action's id, name (menu text), and command name * @param shortDesc a short description of the action (tooltip) * @param longDesc the long description for the action (help possibility) * @param mnemonic the action's mnemonic - for menu traversal * @param accelerator the action's accelerator - "quick key", no menu * @param icon the action's icon (toolbar/menu icon) */ public BasicAction(String id, String shortDesc, String longDesc, Integer mnemonic, KeyStroke accelerator, Icon icon) { this(id, id, id, shortDesc, longDesc, mnemonic, accelerator, icon); } /** * Nearly Full Constructor * @param actionName the action's name (menu text) * @param actionCommandName the name of the action event * @param shortDesc a short description of the action (tooltip) * @param longDesc the long description for the action (help possibility) * @param mnemonic the action's mnemonic - for menu traversal * @param accelerator the action's accelerator - "quick key", no menu * @param icon the action's icon (toolbar/menu icon) */ public BasicAction(String id, String actionName, String actionCommandName, String shortDesc, String longDesc, Integer mnemonic, KeyStroke accelerator, Icon icon) { this(id, id, id, shortDesc, longDesc, mnemonic, accelerator, icon, false, false); } /** * Full Constructor * @param actionName the action's name (menu text) * @param actionCommandName the name of the action event * @param shortDesc a short description of the action (tooltip) * @param longDesc the long description for the action (help possibility) * @param mnemonic the action's mnemonic - for menu traversal * @param accelerator the action's accelerator - "quick key", no menu * @param icon the action's icon (toolbar/menu icon) * @param toolbarShowsText do toolbar buttons created from this action show their text * @param menuShowsIcon do menu items created from this action show their icon */ public BasicAction(String id, String actionName, String actionCommandName, String shortDesc, String longDesc, Integer mnemonic, KeyStroke accelerator, Icon icon, boolean toolbarShowsText, boolean menuShowsIcon) { setId(id); setActionName(actionName); setActionCommandName(actionCommandName); setShortDescription(shortDesc); setLongDescription(longDesc); setMnemonic(mnemonic); setAccelerator(accelerator); setSmallIcon(icon); this.setToolbarShowsText(toolbarShowsText); this.setMenuShowsIcon(menuShowsIcon); } /* * ACTION-RELATED */ /** * Adds a "callback" action listener to this action. The callback allows the * handling of the action to be delegated to other objects. BasicActions override * their actionPerformed method() propogate the event to teh execute() method and * the callback delegates * @param l the callback action listener */ public void addActionListener(ActionListener l) { listenerList.add(ActionListener.class, l); } /** * Removes an callback action listener for this action. * @param l ActionListener */ public void removeActionListener(ActionListener l) { listenerList.remove(ActionListener.class, l); } /** * Implements actionPerformed by calling templateMethod(). Derived classes * can override {@link #actionPerformedTemplate(ActionEvent) actionPerformedTemplate} to change the * default template, but usually don't have to. Instead, derived classes * can override execute() to handle actionPerformed responsiblilities, or * rely on a callback action listener being set. * @param evt the action event * @see #actionPerformedTemplate(java.awt.event.ActionEvent) */ public final void actionPerformed(ActionEvent evt) { actionPerformedTemplate(evt); } /** * The template method for {@link #actionPerformed(ActionEvent)}. * The default template is defined to allow either delegated or derived * implementation. The execute() method is always called. If action listeners are * added to the action, then they are called too. The actionPerformedTry/Catch/Finally methods are * called around the execute()/propogateActionEvent() methods. * <p>To handle application issues such as cursors and threading, the * actionPerformedTry/Catch/Finally methods can be overridden, or the * template method for actionPerformed itself can be changed by overridding * this method. * @param evt the action event sent to actionPerformed() */ protected void actionPerformedTemplate(ActionEvent evt) { try { actionPerformedTry(); execute(evt); propogateActionEvent(evt); } catch (Throwable t) { actionPerformedCatch(t); } finally { actionPerformedFinally(); } } /** * Called during the {@link #actionPerformedTemplate(ActionEvent)} * <p> * Propogates the ActionEvent to ActionListener delegates added via * {@link #addActionListener(ActionListener)} * Called after execute, allowing underlying actions to override * (selectively not calling delegates, for example). * * @param evt ActionEvent received by actionPerformed(ActionEvent). */ protected void propogateActionEvent(ActionEvent evt) { // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == ActionListener.class) { // Lazily create the event: ((ActionListener) listeners[i + 1]).actionPerformed(evt); } } } /** * This method is the first method called during {@link #actionPerformedTemplate(ActionEvent)}. * By default it does nothing. */ protected void actionPerformedTry() { } /** * Typically overridden to do the action's work. * Called during {@link #actionPerformedTemplate(ActionEvent)}. * Derived classes can choose to override this no-op template method or provide an * ActionListener delgate instead by calling {@link #addActionListener(ActionListener)}. * @throws Exception if an exception is throws, actionPerformedTemplate calls * {@link #actionPerformedCatch(Throwable t)} with it. */ protected void execute(ActionEvent evt) throws Exception { } /** * This method is called during {@link #actionPerformedTemplate(ActionEvent)} if the * {@link #execute(ActionEvent)} or any callback ActionListeners. * <p> * The default is pretty lame, it prints the error and stack trace to System.err, then * throws a RuntimeException. * <p> * It is recommended that BasicAction be extended for all an application's * actions, particularly to improve handle exceptions. An easy way to * accomplish this is to create the class and set the defaultActionClass * attribute on the action-set element. */ protected void actionPerformedCatch(Throwable t) { System.err.println("Exception in action "+this+". Exception:"+t); t.printStackTrace(System.err); throw new RuntimeException(t); } /** * This method is the last method called during {@link #actionPerformedTemplate(ActionEvent)} . * By default it does nothing. Derived classes may override this method * to clean up from actionPerformed calls. */ protected void actionPerformedFinally() { } /* * ENABLED-RELATED */ /** * Adds a delegate object to determine enablement during updateEnabledState() * @param enabledDelegate the enablement callback delegate */ public void addShouldBeEnabledDelegate(ShouldBeEnabledDelegate enabledDelegate) { if (enabledDelegates == null) { enabledDelegates = new ArrayList(3); } enabledDelegates.add(enabledDelegate); } /** * Removes a delegate object that determines enablement during updateEnabledState() * @param enabledDelegate the enablement callback delegate */ public void removeShouldBeEnabledDelegate(ShouldBeEnabledDelegate enabledDelegate) { if (enabledDelegates == null) { return; } enabledDelegates.remove(enabledDelegate); } /** * Called to force the action to call setEnabled(boolean). * <p> * If the action can figure it out, it should call setEnabled(false or true) * on itself appropriately. * <p> * By default, this method simply calls setEnabled(shouldBeEnabled()) */ public void updateEnabledState() { boolean shouldBe = shouldBeEnabled(); setEnabled(shouldBe); } /** * Called by clients to ask the action whether it should be enabled or * disabled given the current "state of affairs." * <p> * Makes no change to the action. * <p> * By default, the {@link ShouldBeEnabledDelegate}'s added via addShouldBeEnabledDelegate() are used. * If no ShouldBeEnabledDelegates are set, then true is return (actually DEFAULT_ENABLED_STATE). * If there are any delegates, then if <em>any</em> delegate's shouldBeEnabled() return false * (or !DEFAULT_ENABLED_STATE), then false is called. Only if they all return true (DEFAULT_ENABLED_STATE) * is the true (DEFAULT_ENABLED_STATE) returned. * @return whether setEnabled should be called with false or true */ public boolean shouldBeEnabled() { if (enabledDelegates == null || enabledDelegates.isEmpty()) { return DEFAULT_ENABLED_STATE; } List copy = new ArrayList(enabledDelegates); Iterator iter = copy.iterator(); while (iter.hasNext()) { ShouldBeEnabledDelegate enabledUpdater = (ShouldBeEnabledDelegate) iter.next(); if (enabledUpdater != null) { if (enabledUpdater.shouldBeEnabled(this) != DEFAULT_ENABLED_STATE) { return !DEFAULT_ENABLED_STATE; } } } return DEFAULT_ENABLED_STATE; } /* * ITEM-RELATED */ /** * Adds an item listener for toggle action. Usually not needed since actionPerformed() is * called. Used by the {@link org.bushe.swing.action.ActionSelectionSynchronizer} to synchronize * components that share an action. * @param l and item listener */ public void addItemListener(ItemListener l) { listenerList.add(ItemListener.class, l); } /** * Removes an item listener. Typically not needed. * @param l listener to remove */ public void removeItemListener(ItemListener l) { listenerList.remove(ItemListener.class, l); } /** * Calls all ItemListeners with the ItemEvent * @param evt the event to propogate to listeners. */ protected void propogateItemEvent(ItemEvent evt) { // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == ItemListener.class) { // Lazily create the event: ((ItemListener) listeners[i + 1]).itemStateChanged(evt); } } } /** * @return true if the action is in the selected state value of ActionManager.SELECTED is TRUE */ public boolean isSelected() { Object actionSelected = getValue(ActionManager.SELECTED); return Boolean.TRUE.equals(actionSelected); } /** * Changes the selected state of the action. If selected is different * from the current state, then a "selected" property change is fired. * @param selected true to set the action as selected of the action. */ public synchronized void setSelected(boolean selected) { boolean oldValue = isSelected(); if (oldValue != selected) { Boolean selectedBool = selected?Boolean.TRUE:Boolean.FALSE; putValue(ActionManager.SELECTED, selectedBool); firePropertyChange("selected", oldValue?Boolean.TRUE:Boolean.FALSE, selectedBool); } } /** * @return the action's ActionManager.GROUP value */ public Object getGroup() { return getValue(ActionManager.GROUP); } /* * CONTEXT */ /** * Allows an action to be "contextualized" with a map of object-value pairs * (separate from the the putValue()/getValue() string-value properties of * the action). The context can be set via {@link ActionList#setContextForAll(java.util.Map)} * {@link ActionList#putContextValueForAll(Object, Object)}, * {@link ActionManager#createAction(Object, java.util.Map)}, * {@link ActionManager#createActionList(Object, java.util.Map)}, or * {@link ActionManager#putContextValueForAll(Object, Object)}. * methods or ActionManager's getList and createList methods, or * ActionUIFactory's createMethods. * <p> * The <name-value-pair> elements in Action XML do NOT go into the context, they * go into the normal action putValue()/getValue() set. * <p> * Calls contextChanged(), which calls updateEnabledState * @param context a context object * @see ContextAware */ public void setContext(Map context) { this.context = context; contextChanged(); } /** * Gets the context map for the action. * <p> * Different from put/getValue() string-value pairs and <name-value-pair> elements. * @return the context, can be null */ public Map getContext() { return context; } /** * Clears the action's context. */ public void clearContext() { if (context != null) { context.clear(); } contextChanged(); } /** * Puts a single value in the action's context map. * <p> * Calls contextChanged(). * <p> * Different from put/getValue() string-value pairs and <name-value-pair> elements. * @param key the context key * @param contextValue the context value */ public void putContextValue(Object key, Object contextValue) { if (context == null) { context = new HashMap(3); } context.put(key, contextValue); contextChanged(); } /** * Gets a value out of the context. * <p> * Different from put/getValue() string-value pairs and <name-value-pair> elements. * @return the context value set on this action for the given key, null if not set */ public Object getContextValue(Object key) { if (context == null) { return null; } return context.get(key); } /** * Called when the context is set or a value is added ro removed. * Override to take action on a changing context, such as * enabling or disabling. Default does calls updateEnabledState() */ protected void contextChanged() { updateEnabledState(); } /* * PROPERTIES */ /** * The action name is the name of the action in the application. * Should be unique to it's ActionManager. * @return the id of the action */ public String getId() { return (String) getValue(ActionManager.ID); } /** * Sets the id of the action. * @param id action's ActionManager.ID property value */ public void setId(String id) { putValue(ActionManager.ID, id); } /** * The action name is the name of the action, usually the name of buttons created from the action. * Should be unique to it's universe of listeners. Same as AbstractActions's NAME value. * @return the name of the action */ public String getActionName() { return (String) getValue(NAME); } /** * Sets the NAME of the action. * @param actionName the action's name property */ public void setActionName(String actionName) { putValue(NAME, actionName); } /** * The command string for the ActionEvent that will be created when * this Action is fired. * Should be unique to it's universe of listeners. Same as AbstractActions's * ACTION_COMMAND_KEY value. * @return the name of the action */ public String getActionCommandName() { return (String) getValue(ACTION_COMMAND_KEY); } /** * Sets the ACTION_COMMAND_KEY of the action's command. * @param actionCommandName the action's command's name property */ public void setActionCommandName(String actionCommandName) { putValue(ACTION_COMMAND_KEY, actionCommandName); } /** * The short description for this action, used in toolip text. * @return the SHORT_DESCRIPTION of this action. */ public String getShortDescription() { return (String) getValue(SHORT_DESCRIPTION); } /** * Sets the short description of the action. * @param shortDesc the action's SHORT_DESCRIPTION property */ public void setShortDescription(String shortDesc) { putValue(SHORT_DESCRIPTION, shortDesc); } /** * Used for storing a longer description for the * action, could be used for context-sensitive help * @return longDesc set the long description of this action. */ public String getLongDescription() { return (String) getValue(LONG_DESCRIPTION); } /** * Sets the long description of the action. * @param longDesc the action's long description property */ public void setLongDescription(String longDesc) { putValue(LONG_DESCRIPTION, longDesc); } /** * Mnemonics offer a way to use the keyboard to navigate the menu hierarchy, * increasing the accessibility of programs. Accelerators, on the other hand, * offer keyboard shortcuts to bypass navigating the menu hierarchy. * Mnemonics are for all users; accelerators are for power users. * @return the MNEMONIC_KEY for this action. */ public Integer getMnemonic() { return (Integer) getValue(MNEMONIC_KEY); } /** * @param mnemonic make like so: new Integer(KeyEvent.VK_L) */ protected void setMnemonic(Integer mnemonic) { putValue(MNEMONIC_KEY, mnemonic); } /** * Accelerators offer keyboard shortcuts to bypass navigating the menu * hierarchy. Mnemonics, on the other hand, offer a way to use the * keyboard to navigate the menu hierarchy, increasing the accessibility * of programs. The action must have a menu for accelerators to work. * Mnemonics are for all users; accelerators are for power users. * @return this action's ACCELERATOR_KEY */ public KeyStroke getAccelerator() { return (KeyStroke) getValue(ACCELERATOR_KEY); } /** * Sets the ACCELERATOR_KEY key of the action. * @param accelerator the action's shortcut */ public void setAccelerator(KeyStroke accelerator) { putValue(ACCELERATOR_KEY, accelerator); } /** * @return the SMALL_ICON for the action (used in toolbars) */ public ImageIcon getSmallIcon() { return (ImageIcon) getValue(SMALL_ICON); } /** * Sets the icon of the action. * @param smallIcon the action's icon property */ public void setSmallIcon(Icon smallIcon) { putValue(SMALL_ICON, smallIcon); } /** * Should a toolbar that has this action show the text and the icon or * just the icon? * <p>The default for TOOLBAR_SHOWS_TEXT is false</p> * @param toolbarShowsText true to show text and icon */ public void setToolbarShowsText(boolean toolbarShowsText) { if (toolbarShowsText) { putValue(ActionManager.TOOLBAR_SHOWS_TEXT, Boolean.TRUE); } else { putValue(ActionManager.TOOLBAR_SHOWS_TEXT, Boolean.FALSE); } } /** * Does the toolbar constructed with this action show the text? * @return true if it shows text (TOOLBAR_SHOWS_TEXT property is TRUE) */ public boolean getToolbarShowsText() { Object value = getValue(ActionManager.TOOLBAR_SHOWS_TEXT); if (value == null) { return false; } return ((Boolean)value).booleanValue(); } /** * Should a menu that has this action show the icon and the text or * just the text? * <p>The default for MENU_SHOWS_ICON is false</p> * @param menuShowsIcon true to show text and icon */ public void setMenuShowsIcon(boolean menuShowsIcon) { if (menuShowsIcon) { putValue(ActionManager.MENU_SHOWS_ICON, Boolean.TRUE); } else { putValue(ActionManager.MENU_SHOWS_ICON, Boolean.FALSE); } } /** * Do menus constructed with this action show the icon? * @return true is shows the icon */ public boolean getMenuShowsIcon() { Object value = getValue(ActionManager.MENU_SHOWS_ICON); if (value == null) { return false; } return ((Boolean)value).booleanValue(); } /** * Get the list of role names that this action should be restricted to. * <p> * The action doesn't use this (though it could disable). Instead the ActionManager and * ActionUIFactory use this method (@tood - not yet!) * @return a List of String's */ public List getRoles() { return (List) getValue(ActionManager.ACTION_ROLES); } /** * Set the list of role names that this action should be restricted to. * @param roles a List of String's */ public void setRoles(List roles) { putValue(ActionManager.ACTION_ROLES, roles); } /** * Returns an ImageIcon, or null if the path was invalid. * Example "toolbarButtonGraphics/myImage.gif" * @param resourcePath A path passed this.getClass().getResource(String); * @return a brand new icon, avoiding calling twice for same path */ protected ImageIcon createIcon(String resourcePath) { return createIcon(getClass().getResource(resourcePath)); } /** * Returns an ImageIcon, or null if the path was invalid. * Example "toolbarButtonGraphics/myImage.gif" * @param imageURL A URL to the image * @return a brand new icon, avoiding calling twice for same path */ protected ImageIcon createIcon(java.net.URL imageURL) { if (imageURL == null) { return null; } else { return new ImageIcon(imageURL); } } /** * Overridden to set the idForDebugging for easy debugging and to * use the result of value.intern() if the value is a String. This * allows all keys to be compared using == instead of .equals(). * @param key the key of the property * @param value the value of the property * @see java.lang.String#intern() */ public void putValue(String key, Object value) { if (key != null && value != null) { if (key.equals(ActionManager.ID)) { idForDebugging = value.toString(); } if (value instanceof String) { value = ((String)value).intern(); } } super.putValue(key, value); } /** * Overridden to give a nice string * @return a descriptive string. */ public String toString() { StringBuffer buf = new StringBuffer(100); buf.append(super.toString()+" [id="); buf.append(getId()); buf.append(", enabled="); buf.append(isEnabled()); buf.append(", values={"); Object[] keys = getKeys(); if (keys != null) { for (int i = 0; i < keys.length; i++) { buf.append(keys[i]); buf.append("->"); buf.append(getValue(String.valueOf(keys[i]))); buf.append(";"); } } buf.append("}, context="); buf.append(getContext()); buf.append(", enabled delegates="); buf.append(enabledDelegates); buf.append("]"); return buf.toString(); } }