| ActionFactory.java |
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