001 /* JAPI - (Yet another (hopefully) useful) Java API
002 *
003 * Copyright (C) 2004-2006 Christian Hujer
004 *
005 * This program is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU General Public License as
007 * published by the Free Software Foundation; either version 2 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but
011 * WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013 * General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program; if not, write to the Free Software
017 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
018 * 02111-1307, USA.
019 */
020
021 package net.sf.japi.swing;
022
023 import java.awt.Component;
024 import java.awt.event.ActionEvent;
025 import java.lang.reflect.Field;
026 import java.text.MessageFormat;
027 import java.util.LinkedList;
028 import java.util.List;
029 import java.util.Map;
030 import java.util.MissingResourceException;
031 import java.util.ResourceBundle;
032 import static java.util.ResourceBundle.getBundle;
033 import java.util.WeakHashMap;
034 import java.util.ArrayList;
035 import java.util.prefs.Preferences;
036 import static java.util.prefs.Preferences.userNodeForPackage;
037 import javax.swing.AbstractAction;
038 import javax.swing.Action;
039 import static javax.swing.Action.ACCELERATOR_KEY;
040 import static javax.swing.Action.LONG_DESCRIPTION;
041 import static javax.swing.Action.MNEMONIC_KEY;
042 import static javax.swing.Action.NAME;
043 import static javax.swing.Action.SHORT_DESCRIPTION;
044 import static javax.swing.Action.SMALL_ICON;
045 import javax.swing.ActionMap;
046 import javax.swing.Icon;
047 import javax.swing.JCheckBox;
048 import javax.swing.JLabel;
049 import javax.swing.JMenu;
050 import javax.swing.JMenuBar;
051 import javax.swing.JMenuItem;
052 import javax.swing.JOptionPane;
053 import javax.swing.JToolBar;
054 import static javax.swing.KeyStroke.getKeyStroke;
055 import org.jetbrains.annotations.NonNls;
056 import org.jetbrains.annotations.NotNull;
057 import org.jetbrains.annotations.Nullable;
058 import static net.sf.japi.swing.IconManager.getDefaultIconManager;
059 import static net.sf.japi.swing.ReflectionAction.REFLECTION_MESSAGE_PROVIDER;
060 import static net.sf.japi.swing.ReflectionAction.REFLECTION_TARGET;
061 import static net.sf.japi.swing.ToggleAction.REFLECTION_PROPERTY_NAME;
062
063 /** Class for creating and initializing {@link Action}s that are localized, user configurable and may invoke their final methods using Reflection;
064 * also handles extremely much useful stuff for i18n/l10n.
065 * It is meant as a general service for creating Action objects that have some comfort for the programmer in several aspects:
066 * <ul>
067 * <li>Allow zero or more ResourceBundles to be used when creating Actions</li>
068 * <li>Allow zero or more UserPreferences to be used when creating Actions</li>
069 * <li>Manage an ActionMap to which created Actions are automatically added</li>
070 * </ul>
071 * You may choose to use one or more ActionFactories, depending on the size of your application.
072 * You may use to spread Action configuration information accross one or more ResourceBundles and one or more Preferences, as you wish.
073 * When looking for values, the Preferences are queried first, in addition order, after that the ResourceBundles, again in addition order, until
074 * a value was found. The behaviour when no value was found is undefined.
075 * <h3>Usage</h3>
076 * The recommended usage is to
077 * <ol>
078 * <li>
079 * create and initialize an ActionFactory similar as the following example code, put it somewhere at the start of your program:
080 * <pre>
081 * ActionFactory myActionFactory = ActionFactory.getFactory("MyApplication");
082 * myActionFactory.addBundle("com.mycompany.mypackage.myresource"); // not always required
083 * myActionFactory.addPref(MyClass.class);
084 * </pre>
085 * </li>
086 * <li>
087 * then use the ActionFactory from anywhere within the application like this:
088 * <pre>
089 * ActionFactory myActionFactory = ActionFactory.getFactory("MyApplication");
090 * Action myAction = myActionFactory.createAction("load", this);
091 * </pre>
092 * </li>
093 * </ol>
094 * <p>
095 * All actions created or initialized by an instance of this class are optionally put into that instance's {@link ActionMap}.
096 * If they are stored, you can use that map for later retrieval.
097 * </p>
098 * <h4>Usage Notes: Factory Name</h4>
099 * <ul>
100 * <li>
101 * The factory name is used to try to load a resource bundle when a bundle is created.
102 * The factory name is used as package name for the bundle, the bundle name itself is "action".
103 * Example: When calling <code>ActionFactory.getFactory("net.sf.japi.swing");</code> for the first time, it is tried to load a
104 * {@link ResourceBundle} named <code>net.sf.japi.swing.actions</code> for that <code>ActionFactory</code>.
105 * This automatism has been implemented to save you from the need of initializing an ActionFactory before use.
106 * </li>
107 * </ul>
108 * <h4>Usage Notes: Action Key / Action Name</h4>
109 * The key you supply as first argument of {@link #createAction(boolean, String, Object)} determines several things:
110 * <ul>
111 * <li>The base name for the different keys in the preferences / resource bundle and other known Action Keys:
112 * <table border="1">
113 * <tr><th>What</th><th>Preferences / Bundle key</th><th>Action key if stored in an action</th></tr>
114 * <tr><td>An (somewhat unique) ID</td><td>(<var>basename</var> itself)</td><td>{@link #ACTION_ID}</td></tr>
115 * <tr><td>The icon</td><td><code><var>basename</var> + ".icon"</code></td><td>{@link Action#SMALL_ICON}</td></tr>
116 * <tr><td>The tooltip help</td><td><code><var>basename</var> + ".shortdescription"</code></td><td>{@link Action#SHORT_DESCRIPTION}</td></tr>
117 * <tr><td>The long help</td><td><code><var>basename</var> + ".longdescription"</code></td><td>{@link Action#LONG_DESCRIPTION}</td></tr>
118 * <tr><td>The text label</td><td><code><var>basename</var> + ".text"</code></td><td>{@link Action#NAME}</td></tr>
119 * <tr><td>The keyboard accelerator</td><td><code><var>basename</var> + ".accel"</code></td><td>{@link Action#ACCELERATOR_KEY}</td></tr>
120 * <tr><td>The alternate keyboard accelerator</td><td><code><var>basename</var> + ".accel2"</code></td><td>{@link #ACCELERATOR_KEY_2}</td></tr>
121 * <tr><td>The mnemonic</td><td><code><var>basename</var> + ".mnemonic"</code></td><td>{@link Action#MNEMONIC_KEY}</td></tr>
122 * <tr><td>The method name</td><td></td><td>{@link ReflectionAction#REFLECTION_METHOD_NAME}</td></tr>
123 * <tr><td>The method</td><td></td><td>{@link ReflectionAction#REFLECTION_METHOD}</td></tr>
124 * <tr><td>The boolean property name</td><td></td><td>{@link ToggleAction#REFLECTION_PROPERTY_NAME}</td></tr>
125 * <tr><td>The target instance</td><td></td><td>{@link ReflectionAction#REFLECTION_TARGET}</td></tr>
126 * <tr><td>Exception handler dialogs</td><td><code><var>basename</var> + ".exception." + <var>exception class name</var> + ...</code><br/>The message can be formatted with 1 parameter that will be the localized message of the thrown exception.</td><td>n/a</td></tr>
127 * </table>
128 * </li>
129 * </ul>
130 * <p>Some methods are not related to actions, yet take base keys:</p>
131 * <ul>
132 * <li>The methods for dialogs, e.g. {@link #showMessageDialog(Component, String, Object...)}:
133 * <table border="1">
134 * <tr><th>What</th><th>Preferences / Bundle key</th></tr>
135 * <tr><td>Dialog title</td><td><code><var>basename</var> + ".title"</code></td></tr>
136 * <tr><td>Dialog message</td><td><code><var>basename</var> + ".message"</code></td></tr>
137 * <tr><td>Dialog messagetype </td><td><code><var>basename</var> + ".messagetype"</code><br/>The message type should be one of the message types defined in {@link JOptionPane}, e.g. {@link JOptionPane#PLAIN_MESSAGE}.</td></tr>
138 * </table>
139 * </li>
140 * </ul>
141 * <h4>Final Notes</h4>
142 * <ul>
143 * <li>
144 * If by having read all this you think it might often be a good idea to use a package name as a factory name: this is completely right and the
145 * most common way of using an ActionFactory.
146 * </li>
147 * <li>
148 * If you think you're too lazy to hold your own ActionFactory reference and instead more often call {@link #getFactory(String)}, just go ahead
149 * and do so.
150 * Looking up created ActionFactories is extremely fast, and of course they are initialized exactly once, not more.
151 * </li>
152 * </ul>
153 * @see AbstractAction
154 * @see Action
155 * @see Preferences
156 * @see ResourceBundle
157 * @todo think about toolbar interaction
158 * @todo think about toolbar configuration
159 * @todo eventually rename this ActionBuilder and provide an ActionBuilderFactory.
160 * @author <a href="mailto:chris@riedquat.de">Christian Hujer</a>
161 */
162 public final class ActionFactory {
163
164 /** The key used for storing a somewhat unique id for this Action. */
165 @NotNull public static final String ACTION_ID = "ActionID";
166
167 /** The key used for storing an alternative accelerator for this Action.
168 * Currently unused.
169 */
170 @NotNull public static final String ACCELERATOR_KEY_2 = "AcceleratorKey2";
171
172 /** The ActionFactories. */
173 @NotNull private static final Map<String, ActionFactory> FACTORIES = new WeakHashMap<String, ActionFactory>();
174
175 /** The parent ActionFactories. */
176 @NotNull private final List<ActionFactory> parents = new LinkedList<ActionFactory>();
177
178 /** The ResourceBundles to look for.
179 * Type: ResourceBundle
180 */
181 @NotNull private final List<ResourceBundle> bundles = new LinkedList<ResourceBundle>();
182
183 /** The Preferences to look for.
184 * Type: Preferences
185 */
186 @NotNull private final List<Preferences> prefs = new LinkedList<Preferences>();
187
188 /** The ActionMap to which created Actions are automatically added. */
189 @NotNull private final ActionMap actionMap = new NamedActionMap();
190
191 private List<ActionProvider> actionProviders = new ArrayList<ActionProvider>();
192
193 /** Get an ActionFactory.
194 * If there is no ActionFactory with name <var>key</var>, a new ActionFactory is created and stored.
195 * Future invocations of this method will constantly return that ActionFactory unless the key is garbage collected.
196 * If you must prevent the key from being garbage collected (and with it the ActionFactory), you may internalize the key ({@link String#intern()}).
197 * A good name for a key is the application or package name.
198 * The <code><var>key</var></code> may be a package name, in which case it is tried to load a {@link ResourceBundle} named "action" from that
199 * package and add it ({@link #addBundle(ResourceBundle)}); nothing special happens if that fails.
200 * @param key name of ActionFactory (which even may be <code>null</code> if you are too lazy to invent a key)
201 * @return ActionFactory for given key. The factory is created in case it didn't already exist.
202 */
203 @NotNull public static ActionFactory getFactory(@Nullable final String key) {
204 ActionFactory factory = FACTORIES.get(key);
205 if (factory == null) {
206 factory = new ActionFactory();
207 FACTORIES.put(key, factory);
208 try {
209 factory.addBundle(key + ".action");
210 } catch (final MissingResourceException e) {
211 /* ignore */
212 }
213 // eventually initialize factory here
214 }
215 return factory;
216 }
217
218 /** Add a ResourceBundle to the list of used bundles.
219 * @param baseName the base name of the resource bundle, a fully qualified class name
220 * @see ResourceBundle#getBundle(String)
221 */
222 public void addBundle(@NotNull final String baseName) {
223 //noinspection ConstantConditions
224 if (baseName == null) {
225 throw new NullPointerException("null bundle name not allowed");
226 }
227 @NotNull final ResourceBundle newBundle = getBundle(baseName);
228 addBundle(newBundle);
229 @Nullable final String additionalBundles = newBundle.getString("ActionFactory.additionalBundles");
230 if (additionalBundles != null) {
231 for (final String additionalBundle : additionalBundles.split("\\s+")) {
232 addBundle(additionalBundle);
233 }
234 }
235 }
236
237 /** Method to find the JMenuItem for a specific Action.
238 * @param menuBar JMenuBar to search
239 * @param action Action to find JMenuItem for
240 * @return JMenuItem for action or <code>null</code> if none found
241 * @throws NullPointerException if <var>action</var> or <var>menuBar</var> is <code>null</code>
242 */
243 @Nullable public static JMenuItem find(@NotNull final JMenuBar menuBar, @NotNull final Action action) {
244 //noinspection ConstantConditions
245 if (menuBar == null) {
246 throw new NullPointerException("null JMenuBar not allowed");
247 }
248 //noinspection ConstantConditions
249 if (action == null) {
250 throw new NullPointerException("null Action not allowed");
251 }
252 for (int i = 0; i < menuBar.getMenuCount(); i++) {
253 final JMenu menu = menuBar.getMenu(i);
254 if (menu.getAction() == action) {
255 return menu;
256 } else {
257 final JMenuItem ret = find(menu, action);
258 if (ret != null) {
259 return ret;
260 }
261 }
262 }
263 return null;
264 }
265
266 /** Method to find the JMenuItem for a specific Action.
267 * @param menu JMenu to search
268 * @param action Action to find JMenuItem for
269 * @return JMenuItem for action or <code>null</code> if none found
270 * @throws NullPointerException if <var>menu</var> or <var>action</var> is <code>null</code>
271 */
272 @Nullable public static JMenuItem find(@NotNull final JMenu menu, @NotNull final Action action) {
273 for (int i = 0; i < menu.getItemCount(); i++) {
274 final JMenuItem item = menu.getItem(i);
275 if (item == null) {
276 // Ignore Separators
277 } else if (item.getAction() == action) {
278 return item;
279 } else if (item instanceof JMenu) {
280 final JMenuItem ret = find((JMenu) item, action);
281 if (ret != null) {
282 return ret;
283 }
284 }
285 }
286 return null;
287 }
288
289 /** Create an ActionFactory.
290 * This constructor is private to force users to use the method {@link #getFactory(String)} for recycling ActionFactories and profit of easy
291 * access to the same ActionFactory from within the whole application without passing around ActionFactory references.
292 */
293 private ActionFactory() {
294 }
295
296 /** Get the ActionMap.
297 * @return ActionMap
298 */
299 @NotNull public ActionMap getActionMap() {
300 return actionMap;
301 }
302
303 /** Add a ResourceBundle to the list of used bundles.
304 * @param bundle ResourceBundle to add
305 * @throws NullPointerException if <code>bundle == null</code>
306 */
307 public void addBundle(@NotNull final ResourceBundle bundle) throws NullPointerException {
308 //noinspection ConstantConditions
309 if (bundle == null) {
310 throw new NullPointerException("null ResourceBundle not allowed");
311 }
312 if (!bundles.contains(bundle)) {
313 // insert first because new bundles override old bundles
314 bundles.add(0, bundle);
315 }
316 }
317
318 /** Add a parent to the list of used parents.
319 * @param parent Parent to use if lookups failed in this ActionFactory
320 * WARNING: Adding a descendents as parents of ancestors or vice versa will result in endless recursion and thus stack overflow!
321 * @throws NullPointerException if <code>parent == null</code>
322 */
323 public void addParent(@NotNull final ActionFactory parent) throws NullPointerException {
324 //noinspection ConstantConditions
325 if (parent == null) {
326 throw new NullPointerException("null ActionFactory not allowed");
327 }
328 parents.add(parent);
329 }
330
331 /** Add a Preferences to the list of used preferences.
332 * @param pref Preferences to add
333 * @throws NullPointerException if <code>pref == null</code>
334 */
335 public void addPref(@NotNull final Preferences pref) throws NullPointerException {
336 //noinspection ConstantConditions
337 if (pref == null) {
338 throw new NullPointerException("null ResourceBundle not allowed");
339 }
340 if (!prefs.contains(pref)) {
341 prefs.add(pref);
342 }
343 }
344
345 /** Add a Preferences to the list of used preferences.
346 * @param clazz the class whose package a user preference node is desired
347 * @see Preferences#userNodeForPackage(Class)
348 * @throws NullPointerException if <code>clazz == null</code>
349 */
350 public void addPref(@NotNull final Class<?> clazz) {
351 //noinspection ConstantConditions
352 if (clazz == null) {
353 throw new NullPointerException("null Class not allowed");
354 }
355 addPref(userNodeForPackage(clazz));
356 }
357
358 /** Creates actions.
359 * This is a loop variant of {@link #createAction(boolean,String)}.
360 * The actions created can be retrieved using {@link #getAction(String)} or via the ActionMap returned by {@link #getActionMap()}.
361 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
362 * @param keys Keys of actions to create
363 * @return Array with created actions
364 * @throws NullPointerException in case keys is or contains <code>null</code>
365 */
366 public Action[] createActions(final boolean store, @NotNull final String... keys) throws NullPointerException {
367 final Action[] actions = new Action[keys.length];
368 for (int i = 0; i < keys.length; i++) {
369 actions[i] = createAction(store, keys[i]);
370 }
371 return actions;
372 }
373
374 /** Create an Action.
375 * The created Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
376 * retreive using {@link #getActionMap()}.
377 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
378 * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles and as ActionMap key (may not be
379 * <code>null</code>)
380 * @return created Action, which is a dummy in the sense that its {@link Action#actionPerformed(ActionEvent)} method does not do anything
381 * @throws NullPointerException in case <var>key</var> was <code>null</code>
382 * @see #createAction(boolean,String,Object)
383 * @see #createToggle(boolean,String,Object)
384 * @see #initAction(boolean,Action,String)
385 */
386 public Action createAction(final boolean store, @NotNull final String key) throws NullPointerException {
387 // initAction() checks for null key
388 return initAction(store, new DummyAction(), key);
389 }
390
391 /** Initialize an Action.
392 * This is a convenience method for Action users which want to use the services provided by this {@link ActionFactory} class but need more
393 * sophisticated Action objects they created on their own.
394 * So you can simply create an Action and pass it to this Initialization method to fill its data.
395 * The initialized Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
396 * retreive using {@link #getActionMap()}.
397 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
398 * @param action Action to fill
399 * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles and as ActionMap key (may not be <code>null</code>)
400 * @return the supplied Action object (<var>action</var>) is returned for convenience
401 * @throws NullPointerException in case <var>key</var> was <code>null</code>
402 */
403 @SuppressWarnings({"NestedAssignment"})
404 public Action initAction(final boolean store, @NotNull final Action action, @NotNull final String key) throws NullPointerException {
405 if (key == null) { throw new NullPointerException("null key for Action initialization not allowed."); }
406 action.putValue(ACTION_ID, key);
407 String value;
408 if ((value = getString(key + ".text" )) != null) { action.putValue(NAME, value); }
409 if ((value = getString(key + ".shortdescription")) != null) { action.putValue(SHORT_DESCRIPTION, value); }
410 if ((value = getString(key + ".longdescription" )) != null) { action.putValue(LONG_DESCRIPTION, value); }
411 if ((value = getString(key + ".accel" )) != null) { action.putValue(ACCELERATOR_KEY, getKeyStroke(value)); }
412 if ((value = getString(key + ".accel2" )) != null) { action.putValue(ACCELERATOR_KEY_2, getKeyStroke(value)); }
413 if ((value = getString(key + ".mnemonic" )) != null) { action.putValue(MNEMONIC_KEY, getKeyStroke(value).getKeyCode()); }
414 if ((value = getString(key + ".icon" )) != null) {
415 final Icon image = getDefaultIconManager().getIcon(value);
416 if (image != null) { action.putValue(SMALL_ICON, image); }
417 }
418 action.putValue(REFLECTION_MESSAGE_PROVIDER, this);
419 if (store) {
420 actionMap.put(key, action);
421 }
422 return action;
423 }
424
425 /** Get a String.
426 * First looks one pref after another, in their addition order.
427 * Then looks one bundle after another, in their addition order.
428 * @param key Key to get String for
429 * @return the first value found or <code>null</code> if no value could be found
430 * @throws NullPointerException if <var>key</var> is <code>null</code>
431 */
432 @Nullable public String getString(@NotNull final String key) throws NullPointerException {
433 if (key == null) {
434 throw new NullPointerException("null key not allowed");
435 }
436 String value = null;
437 for (final Preferences pref : prefs) {
438 value = pref.get(key, value);
439 if (value != null) {
440 return value;
441 }
442 }
443 for (final ResourceBundle bundle : bundles) {
444 try {
445 value = bundle.getString(key);
446 return value;
447 } catch (final MissingResourceException e) { /* ignore */
448 } catch (final ClassCastException e) { /* ignore */
449 } // ignore exceptions because they don't mean errors just there's no resource, so parents are checked or null is returned.
450 }
451 for (final ActionFactory parent : parents) {
452 value = parent.getString(key);
453 if (value != null) {
454 return value;
455 }
456 }
457 return null;
458 }
459
460 /** Get a String from the preferences, ignoring the resource bundles.
461 * @param key Key to get String for
462 * @return the first value found or <code>null</code> if no value could be found
463 * @throws NullPointerException if <var>key</var> is <code>null</code>
464 */
465 @Nullable public String getStringFromPrefs(@NotNull final String key) throws NullPointerException {
466 if (key == null) {
467 throw new NullPointerException("null key not allowed");
468 }
469 String value = null;
470 for (final Preferences pref : prefs) {
471 value = pref.get(key, value);
472 if (value != null) {
473 return value;
474 }
475 }
476 for (final ActionFactory parent : parents) {
477 value = parent.getStringFromPrefs(key);
478 if (value != null) {
479 return value;
480 }
481 }
482 return null;
483 }
484
485 /** Get a String from the resource bundles, ignoring the preferences.
486 * @param key Key to get String for
487 * @return the first value found or <code>null</code> if no value could be found
488 * @throws NullPointerException if <var>key</var> is <code>null</code>
489 */
490 @Nullable public String getStringFromBundles(@NotNull final String key) throws NullPointerException {
491 if (key == null) {
492 throw new NullPointerException("null key not allowed");
493 }
494 String value = null;
495 for (final ResourceBundle bundle : bundles) {
496 try {
497 value = bundle.getString(key);
498 return value;
499 } catch (final MissingResourceException e) { /* ignore */
500 } catch (final ClassCastException e) { /* ignore */
501 } // ignore exceptions because they don't mean errors just there's no resource, so parents are checked or null is returned.
502 }
503 for (final ActionFactory parent : parents) {
504 value = parent.getStringFromBundles(key);
505 if (value != null) {
506 return value;
507 }
508 }
509 return null;
510 }
511 /** Creates actions.
512 * This is a loop variant of {@link #createAction(boolean,String,Object)}.
513 * The actions created can be retrieved using {@link #getAction(String)} or via the ActionMap returned by {@link #getActionMap()}.
514 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
515 * @param target Target object
516 * @param keys Keys of actions to create
517 * @return Array with created actions
518 * @throws NullPointerException in case keys is or contains <code>null</code>
519 */
520 public Action[] createActions(final boolean store, final Object target, final String... keys) throws NullPointerException {
521 final Action[] actions = new Action[keys.length];
522 for (int i = 0; i < keys.length; i++) {
523 actions[i] = createAction(store, keys[i], target);
524 }
525 return actions;
526 }
527
528 /** Create an Action.
529 * The created Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
530 * retreive using {@link #getActionMap()}.
531 * The supplied object needs to have a zero argument method named <var>key</var>.
532 * You may pass <code>null</code> as object, which means that the object the method is invoked on is not defined yet.
533 * You may safely use the Action, it will not throw any Exceptions upon {@link Action#actionPerformed(ActionEvent)} but simply silently do nothing.
534 * The desired object can be set later using <code>action.putValue({@link ReflectionAction#REFLECTION_TARGET}, <var>object</var>)</code>.
535 * <p />
536 * Users of this method can assume that the returned object behaves quite like {@link ReflectionAction}.
537 * Wether or not this method returns an instance of {@link ReflectionAction} should not be relied on.
538 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
539 * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles, as ActionMap key and as Reflection Method
540 * name within the supplied object (may not be <code>null</code>)
541 * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(ActionEvent)})
542 * @return created Action
543 * @throws NullPointerException in case <var>key</var> was <code>null</code>
544 * @see #createAction(boolean,String)
545 * @see #createToggle(boolean,String,Object)
546 * @see #initAction(boolean,Action,String)
547 */
548 public Action createAction(final boolean store, final String key, final Object object) throws NullPointerException {
549 if (key == null) { throw new NullPointerException("null key for Action creation not allowed."); }
550 final Action action = new ReflectionAction(key, object);
551 initAction(store, action, key);
552 return action;
553 }
554
555 /** Method for creating a Menu.
556 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
557 * @param menuKey action key for Menu
558 * @param keys Action keys for menu items
559 * @return menu created from the menu definition found
560 * @throws Error in case action definitions for <var>keys</var> weren't found
561 */
562 public JMenu createMenu(final boolean store, final String menuKey, final String... keys) throws Error {
563 final JMenu menu = new JMenu(createAction(store, menuKey));
564 for (final String key : keys) {
565 if (key != null && key.length() == 0) {
566 /* ignore this for empty menus */
567 } else if (key == null || "-".equals(key)) {
568 menu.addSeparator();
569 } else {
570 final Action action = getAction(key);
571 if (action == null) {
572 throw new Error("No Action for key " + key);
573 }
574 if (action instanceof ToggleAction) {
575 menu.add(((ToggleAction) action).createCheckBoxMenuItem());
576 } else {
577 menu.add(action);
578 }
579 }
580 }
581 return menu;
582 }
583
584 /** Get an Action.
585 * For an action to be retrieved with this method, it must have been initialized with {@link #initAction(boolean,Action,String)}, either directly by
586 * invoking {@link #initAction(boolean,Action,String)} or indirectly by invoking one of this class' creation methods like {@link #createAction(boolean,String)},
587 * {@link #createAction(boolean,String,Object)} or {@link #createToggle(boolean,String,Object)}.
588 * @param key Key of action to get
589 * @return Action for <var>key</var>
590 * This method does the same as <code>getActionMap().get(key)</code>.
591 */
592 public Action getAction(final String key) {
593 Action action = actionMap.get(key);
594 if (action == null) {
595 for (final ActionProvider actionProvider : actionProviders) {
596 action = actionProvider.getAction(key);
597 if (action != null) {
598 actionMap.put(key, action);
599 break;
600 }
601 }
602 }
603 return action;
604 }
605
606 /** Method for creating a menubar.
607 * @param store whether to store the initialized Actions in the ActionMap of this ActionFactory
608 * @param barKey Action key of menu to create
609 * @return JMenuBar created for <var>barKey</var>
610 */
611 public JMenuBar createMenuBar(final boolean store, final String barKey) {
612 final JMenuBar menuBar = new JMenuBar();
613 for (final String key : getString(barKey + ".menubar").split("\\s+")) {
614 menuBar.add(createMenu(store, key));
615 }
616 return menuBar;
617 }
618
619 /** Method for creating a Menu.
620 * This method assumes that the underlying properties contain an entry like <code>key + ".menu"</code> which lists the menu element's keys.
621 * Submenus are build recursively.
622 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
623 * @param menuKey action key for menu
624 * @return menu created from the menu definition found
625 * @throws Error in case a menu definition for <var>menuKey</var> wasn't found
626 */
627 public JMenu createMenu(final boolean store, final String menuKey) throws Error {
628 final JMenu menu = new JMenu(createAction(store, menuKey));
629 for (final String key : getString(menuKey + ".menu").split("\\s+")) {
630 if (key != null && key.length() == 0) {
631 /* ignore this for empty menus */
632 } else if (key == null || "-".equals(key)) {
633 menu.addSeparator();
634 } else if (getString(key + ".menu") != null) {
635 menu.add(createMenu(store, key));
636 } else {
637 final Action action = getAction(key);
638 if (action == null) {
639 throw new Error("No Action for key " + key);
640 }
641 if (action instanceof ToggleAction) {
642 menu.add(((ToggleAction) action).createCheckBoxMenuItem());
643 } else {
644 menu.add(action);
645 }
646 }
647 }
648 return menu;
649 }
650
651 /** Method for creating a menubar.
652 * @param store whether to store the initialized Actions in the ActionMap of this ActionFactory
653 * @param barKey Action key of menu to create
654 * @param target Target object
655 * @return JMenuBar created for <var>barKey</var>
656 */
657 public JMenuBar createMenuBar(final boolean store, final String barKey, final Object target) {
658 final JMenuBar menuBar = new JMenuBar();
659 final String menuBarSpec = getString(barKey + ".menubar");
660 if (menuBarSpec == null) {
661 throw new RuntimeException("Missing Resource for " + barKey + ".menubar");
662 }
663 for (final String key : menuBarSpec.split("\\s+")) {
664 menuBar.add(createMenu(store, key, target));
665 }
666 return menuBar;
667 }
668
669 /** Method for creating a Menu.
670 * This method assumes that the underlying properties contain an entry like <code>key + ".menu"</code> which lists the menu element's keys.
671 * Submenus are build recursively.
672 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
673 * @param menuKey action key for menu
674 * @param target Target object
675 * @return menu created from the menu definition found
676 * @throws Error in case a menu definition for <var>menuKey</var> wasn't found
677 */
678 public JMenu createMenu(final boolean store, final String menuKey, final Object target) throws Error {
679 final JMenu menu = new JMenu(createAction(store, menuKey));
680 for (final String key : getString(menuKey + ".menu").split("\\s+")) {
681 if (key != null && key.length() == 0) {
682 /* ignore this for empty menus */
683 } else if (key == null || "-".equals(key)) {
684 menu.addSeparator();
685 } else if (getString(key + ".menu") != null) {
686 menu.add(createMenu(store, key, target));
687 } else {
688 Action action = null;
689 if (store) {
690 action = getAction(key);
691 }
692 if (action == null) {
693 action = createAction(store, key, target);
694 }
695 if (action == null) {
696 throw new Error("No Action for key " + key);
697 }
698 if (action instanceof ToggleAction) {
699 menu.add(((ToggleAction) action).createCheckBoxMenuItem());
700 } else {
701 menu.add(action);
702 }
703 }
704 }
705 return menu;
706 }
707
708 /** Creates actions.
709 * This is a loop variant of {@link #createToggle(boolean,String,Object)}.
710 * The actions created can be retrieved using {@link #getAction(String)} or via the ActionMap returned by {@link #getActionMap()}.
711 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
712 * @param target Target object
713 * @param keys Keys of actions to create
714 * @throws NullPointerException in case <var>keys</var> was or contained <code>null</code>
715 */
716 public void createToggles(final boolean store, final Object target, final String... keys) throws NullPointerException {
717 for (final String key : keys) {
718 createToggle(store, key, target);
719 }
720 }
721
722 /** Create an Action.
723 * The created Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
724 * retreive using {@link #getActionMap()}.
725 * The supplied object needs to have a boolean return void argument getter method and a void return boolean argument setter method matching the
726 * <var>key</var>.
727 * You may pass <code>null</code> as object, which means that the object the getters and setters are invoked on is not defined yet.
728 * You may safely use the Action, it will not throw any Exceptions upon {@link Action#actionPerformed(ActionEvent)} but simply silently do nothing.
729 * The desired object can be set later using <code>action.putValue({@link ToggleAction#REFLECTION_TARGET}, <var>object</var>)</code>.
730 * <p />
731 * Users of this method can assume that the returned object behaves quite like {@link ToggleAction}.
732 * Wether or not this method returns an instance of {@link ToggleAction} shuold not be relied on.
733 * @see #createAction(boolean,String)
734 * @see #createAction(boolean,String,Object)
735 * @see #initAction(boolean,Action,String)
736 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
737 * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles, as ActionMap key and as Property name within
738 * the supplied object (may not be <code>null</code>)
739 * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(ActionEvent)})
740 * @throws NullPointerException in case <var>key</var> was <code>null</code>
741 * @return ToggleAction
742 */
743 public Action createToggle(final boolean store, final String key, final Object object) throws NullPointerException {
744 final Action action = new ToggleAction();
745 initAction(store, action, key);
746 action.putValue(REFLECTION_PROPERTY_NAME, key);
747 action.putValue(REFLECTION_TARGET, object);
748 return action;
749 }
750
751 /** Method for creating a toolbar.
752 * @param keys Action keys for toolbar entries
753 * @return JToolBar created for <var>keys</var>
754 */
755 public JToolBar createToolBar(final String... keys) {
756 final JToolBar toolBar = new JToolBar();
757 for (final String key : keys) {
758 if (key == null || "-".equals(key)) {
759 toolBar.addSeparator();
760 } else {
761 final Action action = getAction(key);
762 if (action == null) {
763 throw new Error("No Action for key " + key);
764 }
765 toolBar.add(action);
766 }
767 }
768 return toolBar;
769 }
770
771 /** Method for creating a toolbar.
772 * @param barKey Action keys of toolbar to create
773 * @return JToolBar created for <var>barKey</var>
774 */
775 public JToolBar createToolBar(final String barKey) {
776 return createToolBar(getString(barKey + ".toolbar").split("\\s+"));
777 }
778
779 /** Method for creating a toolbar.
780 * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(ActionEvent)})
781 * @param keys Action keys for toolbar entries
782 * @return JToolBar created for <var>keys</var>
783 */
784 public JToolBar createToolBar(final Object object, final String... keys) {
785 final JToolBar toolBar = new JToolBar();
786 for (final String key : keys) {
787 if (key == null || "-".equals(key)) {
788 toolBar.addSeparator();
789 } else {
790 Action action = getAction(key);
791 if (action == null) {
792 action = createAction(false, key, object);
793 }
794 toolBar.add(action);
795 }
796 }
797 return toolBar;
798 }
799
800 /** Method for creating a toolbar.
801 * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(ActionEvent)})
802 * @param barKey Action keys of toolbar to create
803 * @return JToolBar created for <var>barKey</var>
804 */
805 public JToolBar createToolBar(final Object object, final String barKey) {
806 return createToolBar(object, getString(barKey + ".toolbar").split("\\s+"));
807 }
808
809 /** Method to find the JMenuItem for a specific Action key.
810 * @param menuBar JMenuBar to search
811 * @param key Key to find JMenuItem for
812 * @return JMenuItem for key or <code>null</code> if none found
813 */
814 public JMenuItem find(final JMenuBar menuBar, final String key) {
815 return find(menuBar, getAction(key));
816 }
817
818 /** Get an icon.
819 * @param key i18n key for icon
820 * @return icon
821 */
822 public Icon getIcon(final String key) {
823 return getDefaultIconManager().getIcon(getString(key));
824 }
825
826 /** Show a localized message dialog.
827 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
828 * no <code>Frame</code>, a default <code>Frame</code> is used
829 * @param messageType the type of message to be displayed
830 * @param key localization key to use for the title, the message and eventually the icon
831 * @param args formatting arguments for the message text
832 * @deprecated use {@link #showMessageDialog(Component, String, Object...)} instead and put the messagetype in the action.properties file.
833 */
834 @Deprecated public void showMessageDialog(final Component parentComponent, final int messageType, final String key, final Object... args) {
835 JOptionPane.showMessageDialog(parentComponent, format(key + ".message", args), format(key + ".title", args), messageType);
836 }
837
838 /** Show a localized message dialog.
839 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
840 * no <code>Frame</code>, a default <code>Frame</code> is used
841 * @param key localization key to use for the title, the message and eventually the icon
842 * @param args formatting arguments for the message text
843 */
844 public void showMessageDialog(final Component parentComponent, final String key, final Object... args) {
845 JOptionPane.showMessageDialog(parentComponent, format(key + ".message", args), format(key + ".title", args), getMessageType(key));
846 }
847
848 /** Get the message type for a dialog.
849 * @param dialogKey dialog key
850 * @return message type
851 */
852 public int getMessageType(final String dialogKey) {
853 final String typeText = getString(dialogKey + ".messageType");
854 if (typeText == null) {
855 return JOptionPane.PLAIN_MESSAGE;
856 }
857 if (!typeText.endsWith("_MESSAGE")) {
858 System.err.println("Warning: Illegal type value " + typeText + " for dialog " + dialogKey); // TODO: I18N/L10N
859 return JOptionPane.PLAIN_MESSAGE;
860 }
861 if ("PLAIN_MESSAGE".equals(typeText)) {
862 return JOptionPane.PLAIN_MESSAGE;
863 } else if ("QUESTION_MESSAGE".equals(typeText)) {
864 return JOptionPane.QUESTION_MESSAGE;
865 } else if ("WARNING_MESSAGE".equals(typeText)) {
866 return JOptionPane.WARNING_MESSAGE;
867 } else if ("INFORMATION_MESSAGE".equals(typeText)) {
868 return JOptionPane.INFORMATION_MESSAGE;
869 } else if ("ERROR_MESSAGE".equals(typeText)) {
870 return JOptionPane.ERROR_MESSAGE;
871 } else {
872 // key not known, try reflection.
873 try {
874 final Field f = JOptionPane.class.getField(typeText);
875 if (f.getType() == Integer.TYPE) {
876 return f.getInt(null);
877 }
878 } catch (final NoSuchFieldException e) {
879 System.err.println("Warning: Field " + typeText + " not found in JOptionPane (dialog: " + dialogKey + ")."); // TODO: I18N/L10N
880 e.printStackTrace(); //TODO
881 } catch (final IllegalAccessException e) {
882 System.err.println("Warning: Field " + typeText + " not accessible in JOptionPane (dialog: " + dialogKey + ")."); // TODO: I18N/L10N
883 e.printStackTrace(); //TODO
884 }
885 return JOptionPane.PLAIN_MESSAGE;
886 }
887 }
888
889 /** Formats a message with parameters.
890 * It's a proxy method for using {@link MessageFormat}.
891 * @param key message key
892 * @param args parameters
893 * @return formatted String
894 * @see MessageFormat#format(String,Object...)
895 */
896 public String format(final String key, final Object... args) {
897 return MessageFormat.format(getString(key), args);
898 }
899
900 /** Show a localized confirmation dialog which the user can suppress in future (remembering his choice).
901 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
902 * no <code>Frame</code>, a default <code>Frame</code> is used
903 * @param optionType the option type
904 * @param messageType the type of message to be displayed
905 * @param key localization key to use for the title, the message and eventually the icon
906 * @param args formatting arguments for the message text
907 * @return an integer indicating the option selected by the user now or eventually in a previous choice
908 * @throws IllegalStateException if no preferences are associated
909 */
910 public int showOnetimeConfirmDialog(final Component parentComponent, final int optionType, final int messageType, final String key, final Object... args) throws IllegalStateException {
911 @NonNls String showString = getString(key + ".show");
912 if (showString == null) {
913 showString = getString(key + ".showDefault");
914 }
915 if (showString == null) {
916 showString = "true";
917 }
918 final boolean show = !"false".equalsIgnoreCase(showString); // undefined should be treated true
919 if (!show) {
920 try {
921 return Integer.parseInt(getString(key + ".choice"));
922 } catch (final Exception ignore) {
923 /* ignore, continue with dialog then. */
924 }
925 }
926 final JCheckBox dontShowAgain = new JCheckBox(getString("dialogDontShowAgain"), true);
927 final int result = JOptionPane.showConfirmDialog(parentComponent, new Object[] { format(key, args), dontShowAgain }, format(key + ".title", args), optionType, messageType);
928 if (!dontShowAgain.isSelected()) {
929 if (prefs.size() > 0) {
930 prefs.get(0).put(key + ".show", "false");
931 prefs.get(0).put(key + ".choice", Integer.toString(result));
932 } else {
933 throw new IllegalStateException("Cannot store prefs for this dialog - no Preferences associated with this ActionFactory!");
934 }
935 }
936 return result;
937 }
938
939 /** Show a localized message dialog which the user can suppress in future.
940 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
941 * no <code>Frame</code>, a default <code>Frame</code> is used
942 * @param messageType the type of message to be displayed
943 * @param key localization key to use for the title, the message and eventually the icon
944 * @param args formatting arguments for the message text
945 * @throws IllegalStateException if no preferences are associated
946 */
947 public void showOnetimeMessageDialog(final Component parentComponent, final int messageType, final String key, final Object... args) throws IllegalStateException {
948 String showString = getString(key + ".show");
949 if (showString == null) {
950 showString = getString(key + ".showDefault");
951 }
952 if (showString == null) {
953 showString = "true";
954 }
955 final boolean show = !"false".equalsIgnoreCase(showString); // undefined should be treated true
956 if (show) {
957 final JCheckBox dontShowAgain = new JCheckBox(getString("dialogDontShowAgain"), true);
958 JOptionPane.showMessageDialog(parentComponent, new Object[] { format(key, args), dontShowAgain }, format(key + ".title", args), messageType);
959 if (!dontShowAgain.isSelected()) {
960 if (prefs.size() > 0) {
961 prefs.get(0).put(key + ".show", "false");
962 } else {
963 throw new IllegalStateException("Cannot store prefs for this dialog - no Preferences associated with this ActionFactory!");
964 }
965 }
966 }
967 }
968
969 /** Show a localized question dialog.
970 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
971 * no <code>Frame</code>, a default <code>Frame</code> is used
972 * @param key localization key to use for the title, the message and eventually the icon
973 * @param args formatting arguments for the message text
974 * @return <code>true</code> if user confirmed, otherwise <code>false</code>
975 */
976 public boolean showQuestionDialog(final Component parentComponent, final String key, final Object... args) {
977 return showConfirmDialog(parentComponent, JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, key, args) == JOptionPane.YES_OPTION;
978 }
979
980 /** Show a localized confirmation dialog.
981 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
982 * no <code>Frame</code>, a default <code>Frame</code> is used
983 * @param optionType the option type
984 * @param messageType the type of message to be displayed
985 * @param key localization key to use for the title, the message and eventually the icon
986 * @param args formatting arguments for the message text
987 * @return an integer indicating the option selected by the user
988 */
989 public int showConfirmDialog(final Component parentComponent, final int optionType, final int messageType, final String key, final Object... args) {
990 return JOptionPane.showConfirmDialog(parentComponent, format(key, args), format(key + ".title", args), optionType, messageType);
991 }
992
993 /** Creates a label for a specified key.
994 * @param key Key to create label for
995 * @param args formatting arguments for the label text
996 * @return JLabel for key Key
997 * @note the label text will be the key if no String for the key was found
998 */
999 public JLabel createLabel(final String key, final Object... args) {
1000 return createLabel((Component) null, key, args);
1001 }
1002
1003 /** Creates a label for a specified key and component.
1004 * @param component Component to associate label to (maybe <code>null</code>)
1005 * @param key Key to create label for
1006 * @param args formatting arguments for the label text
1007 * @return JLabel for key Key
1008 * @note the label text will be the key if no String for the key was found
1009 * @todo support icons
1010 * @todo support mnemonics
1011 * @todo alignments and textpositions
1012 */
1013 public JLabel createLabel(final Component component, final String key, final Object... args) {
1014 final String labelText = format(key, args);
1015 final JLabel label = new JLabel(labelText != null ? labelText : key);
1016 label.setLabelFor(component);
1017 return label;
1018 }
1019
1020 /** Registers an ActionProvider with this ActionFactory.
1021 * @param actionProvider ActionProvider to register
1022 */
1023 public void addActionProvider(final ActionProvider actionProvider) {
1024 actionProviders.add(actionProvider);
1025 }
1026
1027 } // class ActionFactory