1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 as published by
5    * the Free Software Foundation. This program is distributed in the hope
6    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * Copyright: 2005
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: ActionFactory.java 2223 2012-01-26 21:28:02Z dmsmith $
21   */
22  package org.crosswire.common.swing;
23  
24  import java.awt.event.ActionEvent;
25  import java.awt.event.ActionListener;
26  import java.lang.reflect.InvocationTargetException;
27  import java.lang.reflect.Method;
28  import java.util.HashMap;
29  import java.util.Map;
30  
31  import javax.swing.Action;
32  import javax.swing.JButton;
33  import javax.swing.JLabel;
34  
35  import org.crosswire.common.util.Logger;
36  import org.crosswire.common.util.OSType;
37  import org.crosswire.common.util.StringUtil;
38  
39  /**
40   * The ActionFactory is being radically updated. Take the following with a grain of salt.
41   * 
42   * The ActionFactory is responsible for creating CWActions and making them
43   * available to the program. Each Action is constructed from resources of the
44   * form: ActionName.field=value where ActionName is the ACTION_COMMAND_KEY value
45   * and field is one of the CWAction constants, e.g. LargeIcon. <br/>
46   * Field is one of:
47   * <ul>
48   * <li>Name - This is required. The value is used for the text of the Action.<br/>
49   * A mnemonic can be specified by preceding the letter with _. Using this letter in
50   * a case insensitive search, the earliest position of that letter will
51   * cause the it to be underlined. In a platform dependent
52   * way it provides a keyboard mechanism to fire the action. For example, on
53   * Windows, alt + mnemonic will cause a visible, active element with that
54   * mnemonic to fire. For this reason, it is important to ensure that two
55   * visible, active elements do not have the same mnemonic.<br/>
56   * Note: Mnemonics are suppressed on MacOSX.</li>
57   * 
58   * <li>ToolTip - A tip to show when the mouse is over an element. If not
59   * present, Name is used. This is likely to change. It is redundant to show a
60   * tooltip that is identical to the shown text.</li>
61   * 
62   * <li>SmallIcon - A 16x16 pixel image to be shown for the item. The value for
63   * this is a path which can be found as a resource.<br/>
64   * Note: the small icon will be used when actions are tied to menu items and
65   * buttons.</li>
66   * 
67   * <li>LargeIcon - A 24x24 pixel image to be shown for the item when large items
68   * are shown. Currently, these are only used for the ToolBar, when a large
69   * toolbar is requested. The value is a resource path to the image.</li>
70   * 
71   * <li>AcceleratorKey - A key on the keyboard, which may be specified with 0x25
72   * kind of notation.<br/>
73   * <br/>
74   * Accelerators are global key combinations that work within an application to
75   * fire the action. When the action is shown as a menu item the accelerator will
76   * be listed with the name. Note: The accelerator key and it's modifiers are
77   * converted into a <code>KeyStroke</code> with
78   * <code>KeyStroke.getKeyStroke(key, modifierMask);</code></li>
79   * 
80   * <li>AcceleratorKey.Modifier - A comma separated list of ctrl, alt, and shift,
81   * indicating what modifiers are necessary for the accelerator.<br/>
82   * Note: ctrl will use a platform's command key. On MacOSX this is the
83   * Apple/Command key. Other platforms use Ctrl.</li>
84   * <li>Enabled - Defaults to true when not present. It is disabled when the
85   * value does not match "true" regardless of case. This is used to initialize
86   * widgets tied to actions to disabled. Once the action is created, it's state
87   * can be changed and the tied widgets will behave appropriately.</li>
88   * <li>Shared - Defaults to true when not present. It is unshared when the value
89   * does not match "true" regardless of case. When false, each copy of the action
90   * is independent of other copies.</li>
91   * </ul>
92   * 
93   * <p>
94   * In order to facilitate easier translation, Enabled, SmallIcon and LargeIcon
95   * can be specified in a parallel resource, whose name is suffixed with
96   * "_control" as in Desktop_control. This is meant to extrapolate the constant
97   * behavior of an action into a file that probably does not need to be
98   * internationalized. If it does, for example, to suppress the display of icons,
99   * then one would create a resource further suffixed with the language and
100  * perhaps country, as in Desktop_control_fa.
101  * </p>
102  * 
103  * <p>
104  * To add another twist, several actions may have the same name and mnemonic,
105  * differing perhaps by tooltip. To facilitate the sharing of these definitions,
106  * an Aliases resource is defined to contain common values. If the value of a
107  * ActionName.Name is prefixed with "Alias.", as in Go.Name=Alias.Go, then Go
108  * will be used as the ActionName to look up values in the Aliases resource.
109  * </p>
110  * 
111  * <p>
112  * Aliases defines defaults that can be overridden by the referring resource
113  * file. The only value that cannot be overridden is Name.
114  * </p>
115  * 
116  * <p>
117  * When an action is fired, this class, as a listener, reflects the action on
118  * the class providing the resource. For example, DesktopActions creates an
119  * ActionFactory from the Desktop ResourceBundle. When the Exit action is fired,
120  * ActionFactory calls DesktopActions.doExit(ActionEvent event) or
121  * DesktopActions.doExit(), if the first did not exist.
122  * </p>
123  * 
124  * @see gnu.lgpl.License for license details.<br>
125  *      The copyright to this program is held by it's authors.
126  * @author DM Smith [dmsmith555 at yahoo dot com]
127  * @author Joe Walker [joe at eireneh dot com]
128  */
129 public class ActionFactory implements ActionListener, Actionable {
130     /**
131      * Creates an ActionFactory that merely holds actions.
132      * It does not lookup properties to construct an action. Constructing an action is the
133      * responsibility of the calling class. It does not arrange for actions to perform actions.
134      * 
135      */
136     public ActionFactory() {
137         actions = new HashMap<String, CWAction>();
138     }
139 
140     /**
141      * Creates an ActionFactory that merely arranges for actions to be called against a bean.
142      * It does not lookup properties to construct an action. Constructing an action is the
143      * responsibility of the calling class.
144      * 
145      * @param bean
146      */
147     public ActionFactory(Object bean) {
148         this();
149         this.bean = bean;
150     }
151 
152     /* (non-Javadoc)
153      * @see org.crosswire.common.swing.Actionable#actionPerformed(java.lang.String)
154      */
155     public void actionPerformed(String action) {
156         Action act = findAction(action);
157         act.actionPerformed(new ActionEvent(this, 0, action));
158     }
159 
160     /* (non-Javadoc)
161      * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
162      */
163     public void actionPerformed(ActionEvent ev) {
164         String action = ev.getActionCommand();
165 
166         if (action == null || action.length() == 0) {
167             // There's nothing to do.
168             log.error("No action available for: " + bean.getClass().getName());
169             return;
170         }
171 
172         // Instead of cascading if/then/else
173         // use reflection to do a direct lookup and call
174         String methodName = METHOD_PREFIX + action;
175         Exception ex = null;
176         try {
177             try {
178                 Method doMethod = bean.getClass().getDeclaredMethod(methodName, ActionEvent.class);
179                 doMethod.invoke(bean, ev);
180             } catch (NoSuchMethodException e) {
181                 Method doMethod = bean.getClass().getDeclaredMethod(methodName, new Class[0]);
182                 doMethod.invoke(bean, new Object[0]);
183             }
184         } catch (NoSuchMethodException e) {
185             ex = e;
186         } catch (IllegalArgumentException e) {
187             ex = e;
188         } catch (IllegalAccessException e) {
189             ex = e;
190         } catch (InvocationTargetException e) {
191             ex = e;
192         }
193 
194         if (ex != null) {
195             log.error("Could not execute method " + bean.getClass().getName() + "." + methodName + "()", ex);
196         }
197     }
198 
199     /**
200      * Get the Action for the given actionName.
201      * 
202      * @param key
203      *            the internal name of the CWAction
204      * @return CWAction null if it does not exist
205      */
206     public Action findAction(String key) {
207         CWAction action = actions.get(key);
208 
209         if (action == null) {
210             log.info("Missing key: '" + key + "'. Known keys are: " + StringUtil.join(actions.keySet().toArray(), ", "));
211             action = new CWAction();
212             action.putValue(Action.NAME, key);
213             action.putValue(Action.SHORT_DESCRIPTION, MISSING_RESOURCE);
214             action.setEnabled(true);
215             action.addActionListener(this);
216         }
217         return action;
218     }
219 
220     /**
221      * Get the Action for the given actionName.
222      * 
223      * @param key
224      *            the internal name of the CWAction
225      * @return CWAction null if it does not exist
226      * @deprecated use {@link #findAction(String)} instead
227      */
228     @Deprecated
229     public Action getAction(String key) {
230         return findAction(key);
231     }
232 
233     /**
234      * Build a button from an action.
235      * 
236      * @param action
237      *            the action to use
238      * @return the button
239      */
240     public JButton createJButton(CWAction action, ActionListener listener) {
241         CWAction act = action.clone();
242         act.addActionListener(listener);
243         return new JButton(act);
244     }
245 
246     /**
247      * Lookup an existing action for actionName. Otherwise construct, store and return an action.
248      * 
249      * @param key
250      *            The short name by which this action is known. It is used to
251      *            lookup the action for reuse.
252      * @param name
253      *            This is required. The value is used for the text of the
254      *            Action.<br/>
255      *            A mnemonic can be specified by preceding the letter with _.
256      *            Using this letter in a case insensitive search, the earliest
257      *            position of that letter will cause the it to be underlined. In
258      *            a platform dependent way it provides a keyboard mechanism to
259      *            fire the action. For example, on Windows, alt + mnemonic will
260      *            cause a visible, active element with that mnemonic to fire.
261      *            For this reason, it is important to ensure that two visible,
262      *            active elements do not have the same mnemonic.<br/>
263      *            Note: Mnemonics are suppressed on MacOSX.
264      * @return the stored or newly constructed action
265      */
266     public CWAction addAction(String key, String name) {
267         CWAction cwAction = actions.get(key);
268 
269         if (cwAction == null) {
270             cwAction = buildAction(key, name);
271             cwAction.addActionListener(this);
272             actions.put(key, cwAction);
273         }
274 
275         return cwAction;
276     }
277 
278     public CWAction addAction(String key) {
279         return addAction(key, null);
280     }
281 
282     private CWAction buildAction(String key, String name) {
283         if (key == null || key.length() == 0) {
284             log.warn("Key is missing for CWAction");
285         }
286 
287         CWAction cwAction = actions.get(key);
288 
289         if (cwAction != null) {
290             return cwAction;
291         }
292 
293         cwAction = new CWAction();
294         cwAction.putValue(Action.ACTION_COMMAND_KEY, key);
295 
296         // For buttons that are just icons, there may not be a "name" field.
297         if (name != null) {
298             JLabel cwLabel = CWLabel.createJLabel(name);
299             cwAction.putValue(Action.NAME, cwLabel.getText());
300 
301             // Mac's don't have mnemonics.
302             // Otherwise, dig out the mnemonic.
303             if (!OSType.MAC.equals(OSType.getOSType())) {
304                 cwAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(cwLabel.getDisplayedMnemonic()));
305             }
306         }
307 
308         return cwAction;
309     }
310 
311     /**
312      * The tooltip for actions that we generate to paper around missing
313      * resources Normally we would assert, but in live we might want to limp on.
314      */
315     private static final String MISSING_RESOURCE = "Missing Resource";
316 
317     /**
318      * The prefix to methods that we call
319      */
320     private static final String METHOD_PREFIX = "do";
321 
322     /**
323      * The object to which we forward events
324      */
325     private Object bean;
326 
327     /**
328      * The log stream
329      */
330     private static final Logger log = Logger.getLogger(ActionFactory.class);
331 
332     /**
333      * The map of known CWActions
334      */
335     private Map<String, CWAction> actions;
336 }
337