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