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