001 /* JAPI - (Yet anothr (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.text.MessageFormat;
026 import java.util.ArrayList;
027 import java.util.List;
028 import java.util.Map;
029 import java.util.MissingResourceException;
030 import java.util.ResourceBundle;
031 import static java.util.ResourceBundle.getBundle;
032 import java.util.WeakHashMap;
033 import java.util.prefs.Preferences;
034 import static java.util.prefs.Preferences.userNodeForPackage;
035 import javax.swing.Action;
036 import static javax.swing.Action.ACCELERATOR_KEY;
037 import static javax.swing.Action.LONG_DESCRIPTION;
038 import static javax.swing.Action.MNEMONIC_KEY;
039 import static javax.swing.Action.NAME;
040 import static javax.swing.Action.SHORT_DESCRIPTION;
041 import static javax.swing.Action.SMALL_ICON;
042 import javax.swing.ActionMap;
043 import javax.swing.Icon;
044 import javax.swing.JCheckBox;
045 import javax.swing.JMenu;
046 import javax.swing.JMenuBar;
047 import javax.swing.JMenuItem;
048 import javax.swing.JOptionPane;
049 import javax.swing.JToolBar;
050 import static javax.swing.KeyStroke.getKeyStroke;
051 import org.jetbrains.annotations.Nullable;
052 import org.jetbrains.annotations.NotNull;
053 import static net.sf.japi.swing.IconManager.getDefaultIconManager;
054 import static net.sf.japi.swing.ReflectionAction.REFLECTION_TARGET;
055 import static net.sf.japi.swing.ToggleAction.REFLECTION_PROPERTY_NAME;
056
057 /** Class for creating and initializing {@link Action}s that are localized, user configurable and may invoke their final methods using Reflection;
058 * also handles extremely much useful stuff for i18n/l10n.
059 * It is meant as a general service for creating Action objects that have some comfort for the programmer in several aspects:
060 * <ul>
061 * <li>Allow zero or more ResourceBundles to be used when creating Actions</li>
062 * <li>Allow zero or more UserPreferences to be used when creating Actions</li>
063 * <li>Manage an ActionMap to which created Actions are automatically added</li>
064 * </ul>
065 * You may choose to use one or more ActionFactories, depending on the size of your application.
066 * You may use to spread Action configuration information accross one or more ResourceBundles and one or more Preferences, as you wish.
067 * When looking for values, the Preferences are queried first, in addition order, after that the ResourceBundles, again in addition order, until
068 * a value was found. The behaviour when no value was found is undefined.
069 * <h3>Usage</h3>
070 * The recommended usage is to
071 * <ol>
072 * <li>
073 * create and initialize an ActionFactory similar as the following example code, put it somewhere at the start of your program:
074 * <pre>
075 * ActionFactory myActionFactory = ActionFactory.getFactory("MyApplication");
076 * myActionFactory.addBundle("com.mycompany.mypackage.myresource"); // not always required
077 * myActionFactory.addPref(MyClass.class);
078 * </pre>
079 * </li>
080 * <li>
081 * then use the ActionFactory from anywhere within the application like this:
082 * <pre>
083 * ActionFactory myActionFactory = ActionFactory.getFactory("MyApplication");
084 * Action myAction = myActionFactory.createAction("load", this);
085 * </pre>
086 * </li>
087 * </ol>
088 * <p>
089 * All actions created or initialized by an instance of this class are optionally put into that instance's {@link ActionMap}.
090 * If they are stored, you can use that map for later retrieval.
091 * </p>
092 * <h4>Usage Notes: Factory Name</h4>
093 * <ul>
094 * <li>
095 * The factory name is used to try to load a resource bundle when a bundle is created.
096 * The factory name is used as package name for the bundle, the bundle name itself is "action".
097 * Example: When calling <code>ActionFactory.getFactory("com.itcqis.swing");</code> for the first time, it is tried to load a
098 * {@link ResourceBundle} named <code>com.itcqis.swing.actions</code> for that <code>ActionFactory</code>.
099 * This automatism has been implemented to save you from the need of initializing an ActionFactory before use.
100 * </li>
101 * </ul>
102 * <h4>Usage Notes: Action Key / Action Name</h4>
103 * The key you supply as first argument of {@link #createAction(boolean, String, Object)} determines several things:
104 * <ul>
105 * <li>The base name for the different keys in the preferences / resource bundle and other known Action Keys:
106 * <table>
107 * <tr><th>What</th><th>Preferences / Bundle key</th><th>Action key</th></tr>
108 * <tr><td>An (somewhat unique) ID</td><td>(<var>basename</var> itself)</td><td>{@link #ACTION_ID}</td></tr>
109 * <tr><td>The icon</td><td><code><var>basename</var> + ".icon"</code></td><td>{@link Action#SMALL_ICON}</td></tr>
110 * <tr><td>The tooltip help</td><td><code><var>basename</var> + ".shortdescription"</code></td><td>{@link Action#SHORT_DESCRIPTION}</td></tr>
111 * <tr><td>The long help</td><td><code><var>basename</var> + ".longdescription"</code></td><td>{@link Action#LONG_DESCRIPTION}</td></tr>
112 * <tr><td>The text label</td><td><code><var>basename</var> + ".text"</code></td><td>{@link Action#NAME}</td></tr>
113 * <tr><td>The keyboard accelerator</td><td><code><var>basename</var> + ".accel"</code></td><td>{@link Action#ACCELERATOR_KEY}</td></tr>
114 * <tr><td>The alternate keyboard accelerator</td><td><code><var>basename</var> + ".accel2"</code></td><td>{@link #ACCELERATOR_KEY_2}</td></tr>
115 * <tr><td>The mnemonic</td><td><code><var>basename</var> + ".mnemonic"</code></td><td>{@link Action#MNEMONIC_KEY}</td></tr>
116 * <tr><td>The method name</td><td></td><td>{@link ReflectionAction#REFLECTION_METHOD_NAME}</td></tr>
117 * <tr><td>The method</td><td></td><td>{@link ReflectionAction#REFLECTION_METHOD}</td></tr>
118 * <tr><td>The boolean property name</td><td></td><td>{@link ToggleAction#REFLECTION_PROPERTY_NAME}</td></tr>
119 * <tr><td>The target instance</td><td></td><td>{@link ReflectionAction#REFLECTION_TARGET}</td></tr>
120 * </table>
121 * </li>
122 * </ul>
123 * <h4>Final Notes</h4>
124 * <ul>
125 * <li>
126 * 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
127 * most common way of using an ActionFactory.
128 * </li>
129 * <li>
130 * If you think you're too lazy to hold your own ActionFactory reference and instead more often call {@link #getFactory(String)}, just go ahead
131 * and do so.
132 * Looking up created ActionFactories is extremely fast, and of course they are initialized exactly once, not more.
133 * </li>
134 * </ul>
135 * @see javax.swing.AbstractAction
136 * @see Action
137 * @see Preferences
138 * @see ResourceBundle
139 * @todo think about toolbar interaction
140 * @todo think about toolbar configuration
141 * @todo eventually rename this ActionBuilder and provide an ActionBuilderFactory.
142 * @todo storing all Actions in the ActionMap creates memory leaks, using it as param isn't as nice as well, perhaps introduce an ActionProxy interface which handles ActionMaps
143 * @author <a href="mailto:Christian.Hujer@itcqis.com">Christian Hujer</a>
144 */
145 public final class ActionFactory {
146
147 /** The key used for storing a somewhat unique id for this Action. */
148 @NotNull public static final String ACTION_ID = "ActionID";
149
150 /** The key used for storing an alternative accelerator for this Action.
151 * Currently unused.
152 */
153 @NotNull public static final String ACCELERATOR_KEY_2 = "AcceleratorKey2";
154
155 /** The ActionFactories. */
156 @NotNull private static final Map<String, ActionFactory> FACTORIES = new WeakHashMap<String, ActionFactory>();
157
158 /** The parent ActionFactories. */
159 @NotNull private final List<ActionFactory> parents = new ArrayList<ActionFactory>();
160
161 /** The ResourceBundles to look for.
162 * Type: ResourceBundle
163 */
164 @NotNull private final List<ResourceBundle> bundles = new ArrayList<ResourceBundle>();
165
166 /** The Preferences to look for.
167 * Type: Preferences
168 */
169 @NotNull private final List<Preferences> prefs = new ArrayList<Preferences>();
170
171 /** The ActionMap to which created Actions are automatically added. */
172 @NotNull private final ActionMap actionMap = new ActionMap();
173
174 /** Get an ActionFactory.
175 * If there is no ActionFactory with name <var>key</var>, a new ActionFactory is created and stored.
176 * Future invocations of this method will constantly return that ActionFactory unless the key is garbage collected.
177 * If you must prevent the key from being garbage collected (and with it the ActionFactory), you may internalize the key ({@link String#intern()}).
178 * A good name for a key is the application or package name.
179 * 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
180 * package and add it ({@link #addBundle(ResourceBundle)}); nothing special happens if that fails.
181 * @param key name of ActionFactory (which even may be <code>null</code> if you are too lazy to invent a key)
182 * @return ActionFactory for given key. The factory is created in case it didn't already exist.
183 */
184 @NotNull public static ActionFactory getFactory(@Nullable final String key) {
185 ActionFactory factory = FACTORIES.get(key);
186 if (factory == null) {
187 factory = new ActionFactory();
188 FACTORIES.put(key, factory);
189 try {
190 factory.addBundle(key + ".action");
191 } catch (final MissingResourceException e) {
192 /* ignore */
193 }
194 // eventually initialize factory here
195 }
196 return factory;
197 }
198
199 /** Add a ResourceBundle to the list of used bundles.
200 * @param baseName the base name of the resource bundle, a fully qualified class name
201 * @see ResourceBundle#getBundle(String)
202 */
203 public void addBundle(@NotNull final String baseName) {
204 //noinspection ConstantConditions
205 if (baseName == null) {
206 throw new NullPointerException("null bundle name not allowed");
207 }
208 addBundle(getBundle(baseName));
209 }
210
211 /** Method to find the JMenuItem for a specific Action.
212 * @param menuBar JMenuBar to search
213 * @param action Action to find JMenuItem for
214 * @return JMenuItem for action or <code>null</code> if none found
215 * @throws NullPointerException if <var>action</var> or <var>menuBar</var> is <code>null</code>
216 */
217 @Nullable public static JMenuItem find(@NotNull final JMenuBar menuBar, @NotNull final Action action) {
218 //noinspection ConstantConditions
219 if (menuBar == null) {
220 throw new NullPointerException("null JMenuBar not allowed");
221 }
222 //noinspection ConstantConditions
223 if (action == null) {
224 throw new NullPointerException("null Action not allowed");
225 }
226 for (int i = 0; i < menuBar.getMenuCount(); i++) {
227 final JMenu menu = menuBar.getMenu(i);
228 if (menu.getAction() == action) {
229 return menu;
230 } else {
231 final JMenuItem ret = find(menu, action);
232 if (ret != null) {
233 return ret;
234 }
235 }
236 }
237 return null;
238 }
239
240 /** Method ti find the JMenuItem for a specific Action.
241 * @param menu JMenu to search
242 * @param action Action to find JMenuItem for
243 * @return JMenuItem for action or <code>null</code> if none found
244 * @throws NullPointerException if <var>menu</var> or <var>action</var> is <code>null</code>
245 */
246 @Nullable public static JMenuItem find(@NotNull final JMenu menu, @NotNull final Action action) {
247 for (int i = 0; i < menu.getItemCount(); i++) {
248 final JMenuItem item = menu.getItem(i);
249 if (item == null) {
250 // Ignore Separators
251 } else if (item.getAction() == action) {
252 return item;
253 } else if (item instanceof JMenu) {
254 final JMenuItem ret = find((JMenu) item, action);
255 if (ret != null) {
256 return ret;
257 }
258 }
259 }
260 return null;
261 }
262
263 /** Create an ActionFactory.
264 * This constructor is private to force users to use the method {@link #getFactory(String)} for recycling ActionFactories and profit of easy
265 * access to the same ActionFactory from within the whole application without passing around ActionFactory references.
266 */
267 private ActionFactory() {
268 }
269
270 /** Get the ActionMap.
271 * @return ActionMap
272 */
273 @NotNull public ActionMap getActionMap() {
274 return actionMap;
275 }
276
277 /** Add a ResourceBundle to the list of used bundles.
278 * @param bundle ResourceBundle to add
279 * @throws NullPointerException if <code>bundle == null</code>
280 */
281 public void addBundle(@NotNull final ResourceBundle bundle) throws NullPointerException {
282 //noinspection ConstantConditions
283 if (bundle == null) {
284 throw new NullPointerException("null ResourceBundle not allowed");
285 }
286 if (!bundles.contains(bundle)) {
287 bundles.add(bundle);
288 }
289 }
290
291 /** Add a parent to the list of used parents.
292 * @param parent Parent to use if lookups failed in this ActionFactory
293 * WARNING: Adding a descendents as parents of ancestors or vice versa will result in endless recursion and thus stack overflow!
294 * @throws NullPointerException if <code>parent == null</code>
295 */
296 public void addParent(@NotNull final ActionFactory parent) throws NullPointerException {
297 //noinspection ConstantConditions
298 if (parent == null) {
299 throw new NullPointerException("null ActionFactory not allowed");
300 }
301 parents.add(parent);
302 }
303
304 /** Add a Preferences to the list of used preferences.
305 * @param pref Preferences to add
306 * @throws NullPointerException if <code>pref == null</code>
307 */
308 public void addPref(@NotNull final Preferences pref) throws NullPointerException {
309 //noinspection ConstantConditions
310 if (pref == null) {
311 throw new NullPointerException("null ResourceBundle not allowed");
312 }
313 if (!prefs.contains(pref)) {
314 prefs.add(pref);
315 }
316 }
317
318 /** Add a Preferences to the list of used preferences.
319 * @param clazz the class whose package a user preference node is desired
320 * @see Preferences#userNodeForPackage(Class)
321 * @throws NullPointerException if <code>clazz == null</code>
322 */
323 public void addPref(@NotNull final Class<?> clazz) {
324 //noinspection ConstantConditions
325 if (clazz == null) {
326 throw new NullPointerException("null Class not allowed");
327 }
328 addPref(userNodeForPackage(clazz));
329 }
330
331 /** Creates actions.
332 * This is a loop variant of {@link #createAction(boolean,String)}.
333 * The actions created can be retrieved using {@link #getAction(String)} or via the ActionMap returned by {@link #getActionMap()}.
334 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
335 * @param keys Keys of actions to create
336 * @throws NullPointerException in case keys is or contains <code>null</code>
337 */
338 public void createActions(final boolean store, @NotNull final String... keys) throws NullPointerException {
339 for (final String key : keys) {
340 createAction(store, key);
341 }
342 }
343
344 /** Create an Action.
345 * The created Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
346 * retreive using {@link #getActionMap()}.
347 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
348 * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles and as ActionMap key (may not be
349 * <code>null</code>)
350 * @return created Action, which is a dummy in the sense that its {@link Action#actionPerformed(ActionEvent)} method does not do anything
351 * @throws NullPointerException in case <var>key</var> was <code>null</code>
352 * @see #createAction(boolean,String,Object)
353 * @see #createToggle(boolean,String,Object)
354 * @see #initAction(boolean,Action,String)
355 */
356 public Action createAction(final boolean store, @NotNull final String key) throws NullPointerException {
357 // initAction() checks for null key
358 return initAction(store, new DummyAction(), key);
359 }
360
361 /** Initialize an Action.
362 * This is a convenience method for Action users which want to use the services provided by this {@link ActionFactory} class but need more
363 * sophisticated Action objects they created on their own.
364 * So you can simply create an Action and pass it to this Initialization method to fill its data.
365 * The initialized Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
366 * retreive using {@link #getActionMap()}.
367 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
368 * @param action Action to fill
369 * @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>)
370 * @return the supplied Action object (<var>action</var>) is returned for convenience
371 * @throws NullPointerException in case <var>key</var> was <code>null</code>
372 */
373 @SuppressWarnings({"NestedAssignment"})
374 public Action initAction(final boolean store, @NotNull final Action action, @NotNull final String key) throws NullPointerException {
375 if (key == null) { throw new NullPointerException("null key for Action initialization not allowed."); }
376 action.putValue(ACTION_ID, key);
377 String value;
378 if ((value = getString(key + ".text" )) != null) { action.putValue(NAME, value); }
379 if ((value = getString(key + ".shortdescription")) != null) { action.putValue(SHORT_DESCRIPTION, value); }
380 if ((value = getString(key + ".longdescription" )) != null) { action.putValue(LONG_DESCRIPTION, value); }
381 if ((value = getString(key + ".accel" )) != null) { action.putValue(ACCELERATOR_KEY, getKeyStroke(value)); }
382 if ((value = getString(key + ".accel2" )) != null) { action.putValue(ACCELERATOR_KEY_2, getKeyStroke(value)); }
383 if ((value = getString(key + ".mnemonic" )) != null) { action.putValue(MNEMONIC_KEY, getKeyStroke(value).getKeyCode()); }
384 if ((value = getString(key + ".icon" )) != null) {
385 final Icon image = getDefaultIconManager().getIcon(value);
386 if (image != null) { action.putValue(SMALL_ICON, image); }
387 }
388 if (store) {
389 actionMap.put(key, action);
390 }
391 return action;
392 }
393
394 /** Get a String.
395 * First looks one pref after another, in their addition order.
396 * Then looks one bundle after another, in their addition order.
397 * @param key Key to get String for
398 * @return the first value found or <code>null</code> if no value could be found
399 * @throws NullPointerException if <var>key</var> is <code>null</code>
400 */
401 @Nullable public String getString(final String key) throws NullPointerException {
402 if (key == null) {
403 throw new NullPointerException("null key not allowed");
404 }
405 String value = null;
406 for (final Preferences pref : prefs) {
407 value = pref.get(key, value);
408 if (value != null) {
409 return value;
410 }
411 }
412 for (final ResourceBundle bundle : bundles) {
413 try {
414 value = bundle.getString(key);
415 return value;
416 } catch (final MissingResourceException e) { /* ignore */
417 } catch (final ClassCastException e) { /* ignore */
418 } // ignore exceptions because they don't mean errors just there's no resource, so parents are checked or null is returned.
419 }
420 for (final ActionFactory parent : parents) {
421 value = parent.getString(key);
422 if (value != null) {
423 return value;
424 }
425 }
426 return null;
427 }
428
429 /** Creates actions.
430 * This is a loop variant of {@link #createAction(boolean,String,Object)}.
431 * The actions created can be retrieved using {@link #getAction(String)} or via the ActionMap returned by {@link #getActionMap()}.
432 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
433 * @param target Target object
434 * @param keys Keys of actions to create
435 * @throws NullPointerException in case keys is or contains <code>null</code>
436 */
437 public void createActions(final boolean store, final Object target, final String... keys) throws NullPointerException {
438 for (final String key : keys) {
439 createAction(store, key, target);
440 }
441 }
442
443 /** Create an Action.
444 * The created Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
445 * retreive using {@link #getActionMap()}.
446 * The supplied object needs to have a zero argument method named <var>key</var>.
447 * You may pass <code>null</code> as object, which means that the object the method is invoked on is not defined yet.
448 * You may safely use the Action, it will not throw any Exceptions upon {@link Action#actionPerformed(java.awt.event.ActionEvent)} but simply silently do nothing.
449 * The desired object can be set later using <code>action.putValue({@link ReflectionAction#REFLECTION_TARGET}, <var>object</var>)</code>.
450 * <p />
451 * Users of this method can assume that the returned object behaves quite like {@link ReflectionAction}.
452 * Wether or not this method returns an instance of {@link ReflectionAction} should not be relied on.
453 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
454 * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles, as ActionMap key and as Reflection Method
455 * name within the supplied object (may not be <code>null</code>)
456 * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(java.awt.event.ActionEvent)})
457 * @return created Action
458 * @throws NullPointerException in case <var>key</var> was <code>null</code>
459 * @see #createAction(boolean,String)
460 * @see #createToggle(boolean,String,Object)
461 * @see #initAction(boolean,Action,String)
462 */
463 public Action createAction(final boolean store, final String key, final Object object) throws NullPointerException {
464 if (key == null) { throw new NullPointerException("null key for Action creation not allowed."); }
465 final Action action = new ReflectionAction(key, object);
466 initAction(store, action, key);
467 return action;
468 }
469
470 /** Method for creating a Menu.
471 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
472 * @param menuKey action key for Menu
473 * @param keys Action keys for menu items
474 * @return menu created from the menu definition found
475 * @throws Error in case action definitions for <var>keys</var> weren't found
476 */
477 public JMenu createMenu(final boolean store, final String menuKey, final String... keys) throws Error {
478 final JMenu menu = new JMenu(createAction(store, menuKey));
479 for (final String key : keys) {
480 if (key != null && key.length() == 0) {
481 /* ignore this for empty menus */
482 } else if (key == null || key.equals("-")) {
483 menu.addSeparator();
484 } else {
485 final Action action = getAction(key);
486 if (action == null) {
487 throw new Error("No Action for key " + key);
488 }
489 if (action instanceof ToggleAction) {
490 menu.add(((ToggleAction) action).createCheckBoxMenuItem());
491 } else {
492 menu.add(action);
493 }
494 }
495 }
496 return menu;
497 }
498
499 /** Get an Action.
500 * For an action to be retrieved with this method, it must have been initialized with {@link #initAction(boolean,Action,String)}, either directly by
501 * invoking {@link #initAction(boolean,Action,String)} or indirectly by invoking one of this class' creation methods like {@link #createAction(boolean,String)},
502 * {@link #createAction(boolean,String,Object)} or {@link #createToggle(boolean,String,Object)}.
503 * @param key Key of action to get
504 * @return Action for <var>key</var>
505 * This method does the same as <code>getActionMap().get(key)</code>.
506 */
507 public Action getAction(final String key) {
508 return actionMap.get(key);
509 }
510
511 /** Method for creating a menubar.
512 * @param store whether to store the initialized Actions in the ActionMap of this ActionFactory
513 * @param barKey Action key of menu to create
514 * @return JMenuBar created for <var>barKey</var>
515 */
516 public JMenuBar createMenuBar(final boolean store, final String barKey) {
517 final JMenuBar menuBar = new JMenuBar();
518 for (final String key : getString(barKey + ".menubar").split("\\s+")) {
519 menuBar.add(createMenu(store, key));
520 }
521 return menuBar;
522 }
523
524 /** Method for creating a Menu.
525 * This method assumes that the underlying properties contain an entry like <code>key + ".menu"</code> which lists the menu element's keys.
526 * Submenus are build recursively.
527 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
528 * @param menuKey action key for menu
529 * @return menu created from the menu definition found
530 * @throws Error in case a menu definition for <var>menuKey</var> wasn't found
531 */
532 public JMenu createMenu(final boolean store, final String menuKey) throws Error {
533 final JMenu menu = new JMenu(createAction(store, menuKey));
534 for (final String key : getString(menuKey + ".menu").split("\\s+")) {
535 if (key != null && key.length() == 0) {
536 /* ignore this for empty menus */
537 } else if (key == null || key.equals("-")) {
538 menu.addSeparator();
539 } else if (getString(key + ".menu") != null) {
540 menu.add(createMenu(store, key));
541 } else {
542 final Action action = getAction(key);
543 if (action == null) {
544 throw new Error("No Action for key " + key);
545 }
546 if (action instanceof ToggleAction) {
547 menu.add(((ToggleAction) action).createCheckBoxMenuItem());
548 } else {
549 menu.add(action);
550 }
551 }
552 }
553 return menu;
554 }
555
556 /** Method for creating a menubar.
557 * @param store whether to store the initialized Actions in the ActionMap of this ActionFactory
558 * @param barKey Action key of menu to create
559 * @param target Target object
560 * @return JMenuBar created for <var>barKey</var>
561 */
562 public JMenuBar createMenuBar(final boolean store, final String barKey, final Object target) {
563 final JMenuBar menuBar = new JMenuBar();
564 for (final String key : getString(barKey + ".menubar").split("\\s+")) {
565 menuBar.add(createMenu(store, key, target));
566 }
567 return menuBar;
568 }
569
570 /** Method for creating a Menu.
571 * This method assumes that the underlying properties contain an entry like <code>key + ".menu"</code> which lists the menu element's keys.
572 * Submenus are build recursively.
573 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
574 * @param menuKey action key for menu
575 * @param target Target object
576 * @return menu created from the menu definition found
577 * @throws Error in case a menu definition for <var>menuKey</var> wasn't found
578 */
579 public JMenu createMenu(final boolean store, final String menuKey, final Object target) throws Error {
580 final JMenu menu = new JMenu(createAction(store, menuKey));
581 for (final String key : getString(menuKey + ".menu").split("\\s+")) {
582 if (key != null && key.length() == 0) {
583 /* ignore this for empty menus */
584 } else if (key == null || key.equals("-")) {
585 menu.addSeparator();
586 } else if (getString(key + ".menu") != null) {
587 menu.add(createMenu(store, key, target));
588 } else {
589 Action action = null;
590 if (store) {
591 action = getAction(key);
592 }
593 if (action == null) {
594 action = createAction(store, key, target);
595 }
596 if (action == null) {
597 throw new Error("No Action for key " + key);
598 }
599 if (action instanceof ToggleAction) {
600 menu.add(((ToggleAction) action).createCheckBoxMenuItem());
601 } else {
602 menu.add(action);
603 }
604 }
605 }
606 return menu;
607 }
608
609 /** Creates actions.
610 * This is a loop variant of {@link #createToggle(boolean,String,Object)}.
611 * The actions created can be retrieved using {@link #getAction(String)} or via the ActionMap returned by {@link #getActionMap()}.
612 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
613 * @param target Target object
614 * @param keys Keys of actions to create
615 * @throws NullPointerException in case <var>keys</var> was or contained <code>null</code>
616 */
617 public void createToggles(final boolean store, final Object target, final String... keys) throws NullPointerException {
618 for (final String key : keys) {
619 createToggle(store, key, target);
620 }
621 }
622
623 /** Create an Action.
624 * The created Action is automatically stored together with all other Actions created by this Factory instance in an ActionMap, which you can
625 * retreive using {@link #getActionMap()}.
626 * The supplied object needs to have a boolean return void argument getter method and a void return boolean argument setter method matching the
627 * <var>key</var>.
628 * You may pass <code>null</code> as object, which means that the object the getters and setters are invoked on is not defined yet.
629 * You may safely use the Action, it will not throw any Exceptions upon {@link Action#actionPerformed(java.awt.event.ActionEvent)} but simply silently do nothing.
630 * The desired object can be set later using <code>action.putValue({@link ToggleAction#REFLECTION_TARGET}, <var>object</var>)</code>.
631 * <p />
632 * Users of this method can assume that the returned object behaves quite like {@link ToggleAction}.
633 * Wether or not this method returns an instance of {@link ToggleAction} shuold not be relied on.
634 * @see #createAction(boolean,String)
635 * @see #createAction(boolean,String,Object)
636 * @see #initAction(boolean,Action,String)
637 * @param store whether to store the initialized Action in the ActionMap of this ActionFactory
638 * @param key Key for Action, which is used as basename for access to Preferences and ResourceBundles, as ActionMap key and as Property name within
639 * the supplied object (may not be <code>null</code>)
640 * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(java.awt.event.ActionEvent)})
641 * @throws NullPointerException in case <var>key</var> was <code>null</code>
642 * @return ToggleAction
643 */
644 public Action createToggle(final boolean store, final String key, final Object object) throws NullPointerException {
645 final Action action = new ToggleAction();
646 initAction(store, action, key);
647 action.putValue(REFLECTION_PROPERTY_NAME, key);
648 action.putValue(REFLECTION_TARGET, object);
649 return action;
650 }
651
652 /** Method for creating a toolbar.
653 * @param keys Action keys for toolbar entries
654 * @return JToolBar created for <var>keys</var>
655 */
656 public JToolBar createToolBar(final String... keys) {
657 final JToolBar toolBar = new JToolBar();
658 for (final String key : keys) {
659 if (key == null || key.equals("-")) {
660 toolBar.addSeparator();
661 } else {
662 final Action action = getAction(key);
663 if (action == null) {
664 throw new Error("No Action for key " + key);
665 }
666 toolBar.add(action);
667 }
668 }
669 return toolBar;
670 }
671
672 /** Method for creating a toolbar.
673 * @param barKey Action keys of toolbar to create
674 * @return JToolBar created for <var>barKey</var>
675 */
676 public JToolBar createToolBar(final String barKey) {
677 return createToolBar(getString(barKey + ".toolbar").split("\\s+"));
678 }
679
680 /** Method for creating a toolbar.
681 * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(java.awt.event.ActionEvent)})
682 * @param keys Action keys for toolbar entries
683 * @return JToolBar created for <var>keys</var>
684 */
685 public JToolBar createToolBar(final Object object, final String... keys) {
686 final JToolBar toolBar = new JToolBar();
687 for (final String key : keys) {
688 if (key == null || key.equals("-")) {
689 toolBar.addSeparator();
690 } else {
691 toolBar.add(createAction(false, key, object));
692 }
693 }
694 return toolBar;
695 }
696
697 /** Method for creating a toolbar.
698 * @param object Instance to invoke method on if the Action was activated ({@link Action#actionPerformed(java.awt.event.ActionEvent)})
699 * @param barKey Action keys of toolbar to create
700 * @return JToolBar created for <var>barKey</var>
701 */
702 public JToolBar createToolBar(final Object object, final String barKey) {
703 return createToolBar(object, getString(barKey + ".toolbar").split("\\s+"));
704 }
705
706 /** Method to find the JMenuItem for a specific Action key.
707 * @param menuBar JMenuBar to search
708 * @param key Key to find JMenuItem for
709 * @return JMenuItem for key or <code>null</code> if none found
710 */
711 public JMenuItem find(final JMenuBar menuBar, final String key) {
712 return find(menuBar, getAction(key));
713 }
714
715 /** Get an icon.
716 * @param key i18n key for icon
717 * @return icon
718 */
719 public Icon getIcon(final String key) {
720 return getDefaultIconManager().getIcon(getString(key));
721 }
722
723 /** Show a localized message dialog.
724 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
725 * no <code>Frame</code>, a default <code>Frame</code> is used
726 * @param messageType the type of message to be displayed
727 * @param key localization key to use for the title, the message and eventually the icon
728 * @param args formatting arguments for the message text
729 */
730 public void showMessageDialog(final Component parentComponent, final int messageType, final String key, final Object... args) {
731 JOptionPane.showMessageDialog(parentComponent, format(key, args), getString(key + ".title"), messageType);
732 }
733
734 /** Formats a message with parameters.
735 * It's a proxy method for using {@link MessageFormat}.
736 * @param key message key
737 * @param args parameters
738 * @return formatted String
739 * @see MessageFormat#format(String,Object...)
740 */
741 public String format(final String key, final Object... args) {
742 return MessageFormat.format(getString(key), args);
743 }
744
745 /** Show a localized confirmation dialog which the user can suppress in future (remembering his choice).
746 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
747 * no <code>Frame</code>, a default <code>Frame</code> is used
748 * @param optionType the option type
749 * @param messageType the type of message to be displayed
750 * @param key localization key to use for the title, the message and eventually the icon
751 * @param args formatting arguments for the message text
752 * @return an integer indicating the option selected by the user now or eventually in a previous choice
753 * @throws IllegalStateException if no preferences are associated
754 */
755 public int showOnetimeConfirmDialog(final Component parentComponent, final int optionType, final int messageType, final String key, final Object... args) throws IllegalStateException {
756 String showString = getString(key + ".show");
757 if (showString == null) {
758 showString = getString(key + ".showDefault");
759 }
760 if (showString == null) {
761 showString = "true";
762 }
763 final boolean show = !"false".equalsIgnoreCase(showString); // undefined should be treated true
764 if (!show) {
765 try {
766 return Integer.parseInt(getString(key + ".choice"));
767 } catch (final Exception e) {
768 /* ignore, continue with dialog then. */
769 }
770 }
771 final JCheckBox dontShowAgain = new JCheckBox(getString("dialogDontShowAgain"), true);
772 final int result = JOptionPane.showConfirmDialog(parentComponent, new Object[] { format(key, args), dontShowAgain }, getString(key + ".title"), optionType, messageType);
773 if (!dontShowAgain.isSelected()) {
774 if (prefs.size() > 0) {
775 prefs.get(0).put(key + ".show", "false");
776 prefs.get(0).put(key + ".choice", Integer.toString(result));
777 } else {
778 throw new IllegalStateException("Cannot store prefs for this dialog - no Preferences associated with this ActionFactory!");
779 }
780 }
781 return result;
782 }
783
784 /** Show a localized message dialog which the user can suppress in future.
785 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
786 * no <code>Frame</code>, a default <code>Frame</code> is used
787 * @param messageType the type of message to be displayed
788 * @param key localization key to use for the title, the message and eventually the icon
789 * @param args formatting arguments for the message text
790 * @throws IllegalStateException if no preferences are associated
791 */
792 public void showOnetimeMessageDialog(final Component parentComponent, final int messageType, final String key, final Object... args) throws IllegalStateException {
793 String showString = getString(key + ".show");
794 if (showString == null) {
795 showString = getString(key + ".showDefault");
796 }
797 if (showString == null) {
798 showString = "true";
799 }
800 final boolean show = !"false".equalsIgnoreCase(showString); // undefined should be treated true
801 if (show) {
802 final JCheckBox dontShowAgain = new JCheckBox(getString("dialogDontShowAgain"), true);
803 JOptionPane.showMessageDialog(parentComponent, new Object[] { format(key, args), dontShowAgain }, getString(key + ".title"), messageType);
804 if (!dontShowAgain.isSelected()) {
805 if (prefs.size() > 0) {
806 prefs.get(0).put(key + ".show", "false");
807 } else {
808 throw new IllegalStateException("Cannot store prefs for this dialog - no Preferences associated with this ActionFactory!");
809 }
810 }
811 }
812 }
813
814 /** Show a localized question dialog.
815 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
816 * no <code>Frame</code>, a default <code>Frame</code> is used
817 * @param key localization key to use for the title, the message and eventually the icon
818 * @param args formatting arguments for the message text
819 * @return <code>true</code> if user confirmed, otherwise <code>false</code>
820 */
821 public boolean showQuestionDialog(final Component parentComponent, final String key, final Object... args) {
822 return showConfirmDialog(parentComponent, JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, key, args) == JOptionPane.YES_OPTION;
823 }
824
825 /** Show a localized confirmation dialog.
826 * @param parentComponent determines the Frame in which the dialog is displayed; if <code>null</code>, or if the <code>parentComponent</code> has
827 * no <code>Frame</code>, a default <code>Frame</code> is used
828 * @param optionType the option type
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 * @return an integer indicating the option selected by the user
833 */
834 public int showConfirmDialog(final Component parentComponent, final int optionType, final int messageType, final String key, final Object... args) {
835 return JOptionPane.showConfirmDialog(parentComponent, format(key, args), getString(key + ".title"), optionType, messageType);
836 }
837
838 } // class ActionFactory