001 /*
002 * $Id: ActionContainerFactory.java 1265 2006-07-21 17:15:39Z kleopatra $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
020 */
021 package org.jdesktop.swingx.action;
022
023 import java.awt.Insets;
024 import java.beans.PropertyChangeListener;
025 import java.util.Arrays;
026 import java.util.HashMap;
027 import java.util.Iterator;
028 import java.util.List;
029 import java.util.Map;
030
031 import javax.swing.AbstractButton;
032 import javax.swing.Action;
033 import javax.swing.ButtonGroup;
034 import javax.swing.Icon;
035 import javax.swing.JButton;
036 import javax.swing.JCheckBoxMenuItem;
037 import javax.swing.JComponent;
038 import javax.swing.JMenu;
039 import javax.swing.JMenuBar;
040 import javax.swing.JMenuItem;
041 import javax.swing.JPopupMenu;
042 import javax.swing.JRadioButtonMenuItem;
043 import javax.swing.JToggleButton;
044 import javax.swing.JToolBar;
045
046 /**
047 * Creates user interface elements based on action ids and lists of action ids.
048 * All action ids must represent actions managed by the ActionManager.
049 * <p>
050 * <h3>Action Lists</h3>
051 * Use the createXXX(List) methods to construct containers of actions like menu
052 * bars, menus, popups and toolbars from actions represented as action ids in a
053 * <i>java.util.List</i>. Each element in the action-list can be one of 3 types:
054 * <ul>
055 * <li>action id: corresponds to an action managed by the ActionManager
056 * <li>null: indicates a separator should be inserted.
057 * <li>java.util.List: represents a submenu. See the note below which describes
058 * the configuration of menus.
059 * </li>
060 * The order of elements in an action-list determines the arrangement of the ui
061 * components which are contructed from the action-list.
062 * <p>
063 * For a menu or submenu, the first element in the action-list represents a menu
064 * and subsequent elements represent menu items or separators (if null).
065 * <p>
066 * This class can be used as a general component factory which will construct
067 * components from Actions if the <code>create<comp>(Action,...)</code>
068 * methods are used.
069 *
070 * @see ActionManager
071 */
072 public class ActionContainerFactory {
073 /**
074 * Standard margin for toolbar buttons to improve their look
075 */
076 private static Insets TOOLBAR_BUTTON_MARGIN = new Insets(1, 1, 1, 1);
077
078 private ActionManager manager;
079
080 // Map between group id + component and the ButtonGroup
081 private Map groupMap;
082
083 /**
084 * Constructs an container factory which uses the default
085 * ActionManager.
086 *
087 */
088 public ActionContainerFactory() {
089 }
090 /**
091 * Constructs an container factory which uses managed actions.
092 *
093 * @param manager use the actions managed with this manager for
094 * constructing ui componenents.
095 */
096 public ActionContainerFactory(ActionManager manager) {
097 setActionManager(manager);
098 }
099
100 /**
101 * Gets the ActionManager instance. If the ActionManager has not been explicitly
102 * set then the default ActionManager instance will be used.
103 *
104 * @return the ActionManager used by the ActionContainerFactory.
105 * @see #setActionManager
106 */
107 public ActionManager getActionManager() {
108 if (manager == null) {
109 manager = ActionManager.getInstance();
110 }
111 return manager;
112 }
113
114 /**
115 * Sets the ActionManager instance that will be used by this
116 * ActionContainerFactory
117 */
118 public void setActionManager(ActionManager manager) {
119 this.manager = manager;
120 }
121
122 /**
123 * Constructs a toolbar from an action-list id. By convention,
124 * the identifier of the main toolbar should be "main-toolbar"
125 *
126 * @param list a list of action ids used to construct the toolbar.
127 * @return the toolbar or null
128 */
129 public JToolBar createToolBar(Object[] list) {
130 return createToolBar(Arrays.asList(list));
131 }
132
133 /**
134 * Constructs a toolbar from an action-list id. By convention,
135 * the identifier of the main toolbar should be "main-toolbar"
136 *
137 * @param list a list of action ids used to construct the toolbar.
138 * @return the toolbar or null
139 */
140 public JToolBar createToolBar(List list) {
141 JToolBar toolbar = new JToolBar();
142 Iterator iter = list.iterator();
143 while(iter.hasNext()) {
144 Object element = iter.next();
145
146 if (element == null) {
147 toolbar.addSeparator();
148 } else {
149 AbstractButton button = createButton(element, toolbar);
150 // toolbar buttons shouldn't steal focus
151 button.setFocusable(false);
152 /*
153 * TODO
154 * The next two lines improve the default look of the buttons.
155 * This code should be changed to retrieve the default look
156 * from some UIDefaults object.
157 */
158 button.setMargin(TOOLBAR_BUTTON_MARGIN);
159 button.setBorderPainted(false);
160
161 toolbar.add(button);
162 }
163 }
164 return toolbar;
165 }
166
167
168 /**
169 * Constructs a popup menu from an array of action ids.
170 *
171 * @param list an array of action ids used to construct the popup.
172 * @return the popup or null
173 */
174 public JPopupMenu createPopup(Object[] list) {
175 return createPopup(Arrays.asList(list));
176 }
177
178 /**
179 * Constructs a popup menu from a list of action ids.
180 *
181 * @param list a list of action ids used to construct the popup.
182 * @return the popup or null
183 */
184 public JPopupMenu createPopup(List list) {
185 JPopupMenu popup = new JPopupMenu();
186 Iterator iter = list.iterator();
187 while(iter.hasNext()) {
188 Object element = iter.next();
189
190 if (element == null) {
191 popup.addSeparator();
192 } else if (element instanceof List) {
193 JMenu newMenu= createMenu((List)element);
194 if (newMenu!= null) {
195 popup.add(newMenu);
196 }
197 } else {
198 popup.add(createMenuItem(element, popup));
199 }
200 }
201 return popup;
202 }
203
204 /**
205 * Constructs a menu tree from a list of actions or lists of lists or actions.
206 *
207 * TODO This method is broken. It <em>should</em> expect either that every
208 * entry is a List (thus, the sub menus off the main MenuBar), or it should
209 * handle normal actions properly. By submitting a List of all Actions, nothing
210 * is created....
211 * <p>
212 * For example, If my list is [action, action, action], then nothing is added
213 * to the menu bar. However, if my list is [list[action], action, action, action] then
214 * I get a menu and under it the tree actions. This should not be, because if I
215 * wanted those actions to be on the sub menu, then they should have been
216 * listed within the sub list!
217 *
218 * @param actionIds an array which represents the root item.
219 * @return a menu bar which represents the menu bar tree
220 */
221 public JMenuBar createMenuBar(Object[] actionIds) {
222 return createMenuBar(Arrays.asList(actionIds));
223 }
224
225 /**
226 * Constructs a menu tree from a list of actions or lists of lists or actions.
227 * TODO This method is broken. It <em>should</em> expect either that every
228 * entry is a List (thus, the sub menus off the main MenuBar), or it should
229 * handle normal actions properly. By submitting a List of all Actions, nothing
230 * is created....
231 * <p>
232 * For example, If my list is [action, action, action], then nothing is added
233 * to the menu bar. However, if my list is [list[action], action, action, action] then
234 * I get a menu and under it the tree actions. This should not be, because if I
235 * wanted those actions to be on the sub menu, then they should have been
236 * listed within the sub list!
237 *
238 * @param list a list which represents the root item.
239 * @return a menu bar which represents the menu bar tree
240 */
241 public JMenuBar createMenuBar(List list) {
242 JMenuBar menubar = new JMenuBar();
243 JMenu menu = null;
244
245 Iterator iter = list.iterator();
246 while(iter.hasNext()) {
247 Object element = iter.next();
248
249 if (element == null) {
250 if (menu != null) {
251 menu.addSeparator();
252 }
253 } else if (element instanceof List) {
254 menu = createMenu((List)element);
255 if (menu != null) {
256 menubar.add(menu);
257 }
258 } else {
259 if (menu != null) {
260 menu.add(createMenuItem(element, menu));
261 }
262 }
263 }
264 return menubar;
265 }
266
267
268 /**
269 * Creates and returns a menu from a List which represents actions, separators
270 * and sub-menus. The menu
271 * constructed will have the attributes from the first action in the List.
272 * Subsequent actions in the list represent menu items.
273 *
274 * @param actionIds an array of action ids used to construct the menu and menu items.
275 * the first element represents the action used for the menu,
276 * @return the constructed JMenu or null
277 */
278 public JMenu createMenu(Object[] actionIds) {
279 return createMenu(Arrays.asList(actionIds));
280 }
281
282 /**
283 * Creates and returns a menu from a List which represents actions, separators
284 * and sub-menus. The menu
285 * constructed will have the attributes from the first action in the List.
286 * Subsequent actions in the list represent menu items.
287 *
288 * @param list a list of action ids used to construct the menu and menu items.
289 * the first element represents the action used for the menu,
290 * @return the constructed JMenu or null
291 */
292 public JMenu createMenu(List list) {
293 // The first item will be the action for the JMenu
294 Action action = getAction(list.get(0));
295 if (action == null) {
296 return null;
297 }
298 JMenu menu = new JMenu(action);
299
300 // The rest of the items represent the menu items.
301 Iterator iter = list.listIterator(1);
302 while(iter.hasNext()) {
303 Object element = iter.next();
304 if (element == null) {
305 menu.addSeparator();
306 } else if (element instanceof List) {
307 JMenu newMenu = createMenu((List)element);
308 if (newMenu != null) {
309 menu.add(newMenu);
310 }
311 } else {
312 menu.add(createMenuItem(element, menu));
313 }
314 }
315 return menu;
316 }
317
318
319 /**
320 * Convenience method to get the action from an ActionManager.
321 */
322 private Action getAction(Object id) {
323 Action action = getActionManager().getAction(id);
324 if (action == null) {
325 throw new RuntimeException("ERROR: No Action for " + id);
326 }
327 return action;
328 }
329
330 /**
331 * Returns the button group corresponding to the groupid
332 *
333 * @param groupid the value of the groupid attribute for the action element
334 * @param container a container which will further identify the ButtonGroup
335 */
336 private ButtonGroup getGroup(String groupid, JComponent container) {
337 if (groupMap == null) {
338 groupMap = new HashMap();
339 }
340 int intCode = groupid.hashCode();
341 if (container != null) {
342 intCode ^= container.hashCode();
343 }
344 Integer hashCode = new Integer(intCode);
345
346 ButtonGroup group = (ButtonGroup)groupMap.get(hashCode);
347 if (group == null) {
348 group = new ButtonGroup();
349 groupMap.put(hashCode, group);
350 }
351 return group;
352 }
353
354 /**
355 * Creates a menu item based on the attributes of the action element.
356 * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem
357 * depending on the context of the Action.
358 *
359 * @return a JMenuItem or subclass depending on type.
360 */
361 private JMenuItem createMenuItem(Object id, JComponent container) {
362 return createMenuItem(getAction(id), container);
363 }
364
365
366 /**
367 * Creates a menu item based on the attributes of the action element.
368 * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem
369 * depending on the context of the Action.
370 *
371 * @param action a mangaged Action
372 * @param container the parent container may be null for non-group actions.
373 * @return a JMenuItem or subclass depending on type.
374 */
375 public JMenuItem createMenuItem(Action action, JComponent container) {
376 JMenuItem menuItem = null;
377 if (action instanceof AbstractActionExt) {
378 AbstractActionExt ta = (AbstractActionExt)action;
379
380 if (ta.isStateAction()) {
381 String groupid = (String)ta.getGroup();
382 if (groupid != null) {
383 // If this action has a groupid attribute then it's a
384 // GroupAction
385 menuItem = createRadioButtonMenuItem(getGroup(groupid, container),
386 (AbstractActionExt)action);
387 } else {
388 menuItem = createCheckBoxMenuItem((AbstractActionExt)action);
389 }
390 }
391 }
392
393 if (menuItem == null) {
394 menuItem= new JMenuItem(action);
395 configureMenuItemFromExtActionProperties(menuItem, action);
396 }
397 return menuItem;
398 }
399
400 /**
401 * Creates a menu item based on the attributes of the action.
402 * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem
403 * depending on the context of the Action.
404 *
405 * @param action an action used to create the menu item
406 * @return a JMenuItem or subclass depending on type.
407 */
408 public JMenuItem createMenuItem(Action action) {
409 return createMenuItem(action, null);
410 }
411
412
413 /**
414 * Creates, configures and returns an AbstractButton.
415 *
416 * The attributes of the action element
417 * registered with the ActionManger by the given id.
418 * Will return a JButton or a JToggleButton.
419 *
420 * @param id the identifer
421 * @param container the JComponent which parents the group, if any.
422 * @return an AbstractButton based on the
423 */
424 public AbstractButton createButton(Object id, JComponent container) {
425 return createButton(getAction(id), container);
426 }
427
428 /**
429 * Creates a button based on the attributes of the action. If the container
430 * parameter is non-null then it will be used to uniquely identify
431 * the returned component within a ButtonGroup. If the action doesn't
432 * represent a grouped component then this value can be null.
433 *
434 * @param action an action used to create the button
435 * @param container the parent container to uniquely identify
436 * grouped components or null
437 * @return will return a JButton or a JToggleButton.
438 */
439 public AbstractButton createButton(Action action, JComponent container) {
440 if (action == null) {
441 return null;
442 }
443
444 AbstractButton button = null;
445 if (action instanceof AbstractActionExt) {
446 // Check to see if we should create a toggle button
447 AbstractActionExt ta = (AbstractActionExt)action;
448
449 if (ta.isStateAction()) {
450 // If this action has a groupid attribute then it's a
451 // GroupAction
452 String groupid = (String)ta.getGroup();
453 if (groupid == null) {
454 button = createToggleButton(ta);
455 } else {
456 button = createToggleButton(ta, getGroup(groupid, container));
457 }
458 }
459 }
460
461 if (button == null) {
462 // Create a regular button
463 button = new JButton(action);
464 configureButtonFromExtActionProperties(button, action);
465 }
466 return button;
467 }
468
469 /**
470 * Creates a button based on the attributes of the action.
471 *
472 * @param action an action used to create the button
473 * @return will return a JButton or a JToggleButton.
474 */
475 public AbstractButton createButton(Action action) {
476 return createButton(action, null);
477 }
478
479 /**
480 * Adds and configures a toggle button.
481 * @param a an abstraction of a toggle action.
482 */
483 private JToggleButton createToggleButton(AbstractActionExt a) {
484 return createToggleButton(a, null);
485 }
486
487 /**
488 * Adds and configures a toggle button.
489 * @param a an abstraction of a toggle action.
490 * @param group the group to add the toggle button or null
491 */
492 private JToggleButton createToggleButton(AbstractActionExt a, ButtonGroup group) {
493 JToggleButton button = new JToggleButton();
494 configureButton(button, a, group);
495 return button;
496 }
497
498 /**
499 *
500 * @param button
501 * @param a
502 * @param group
503 */
504 public void configureButton(JToggleButton button, AbstractActionExt a, ButtonGroup group) {
505 configureSelectableButton(button, a, group);
506 configureButtonFromExtActionProperties(button, a);
507 }
508
509 /**
510 * method to configure a "selectable" button from the given AbstractActionExt.
511 * As there is some un-/wiring involved to support synch of the selected property between
512 * the action and the button, all config and unconfig (== setting a null action!)
513 * should be passed through this method. <p>
514 *
515 * It's up to the client to only pass in button's where selected and/or the
516 * group property makes sense.
517 *
518 * PENDING: the group properties are yet untested.
519 * PENDING: think about automated unconfig.
520 *
521 * @param button where selected makes sense
522 * @param a
523 * @param group the button should be added to.
524 * @throws IllegalArgumentException if the given action doesn't have the state flag set.
525 *
526 */
527 public void configureSelectableButton(AbstractButton button, AbstractActionExt a, ButtonGroup group){
528 if ((a != null) && !a.isStateAction()) throw
529 new IllegalArgumentException("the Action must be a stateAction");
530 // we assume that all button configuration is done exclusively through this method!!
531 if (button.getAction() == a) return;
532
533 // unconfigure if the old Action is a state AbstractActionExt
534 // PENDING JW: automate unconfigure via a PCL that is listening to
535 // the button's action property? Think about memory leak implications!
536 Action oldAction = button.getAction();
537 if (oldAction instanceof AbstractActionExt) {
538 AbstractActionExt actionExt = (AbstractActionExt) oldAction;
539 // remove as itemListener
540 button.removeItemListener(actionExt);
541 // remove the button related PCL from the old actionExt
542 PropertyChangeListener[] l = actionExt.getPropertyChangeListeners();
543 for (int i = l.length - 1; i >= 0; i--) {
544 if (l[i] instanceof ToggleActionPropertyChangeListener) {
545 ToggleActionPropertyChangeListener togglePCL = (ToggleActionPropertyChangeListener) l[i];
546 if (togglePCL.isToggling(button)) {
547 actionExt.removePropertyChangeListener(togglePCL);
548 }
549 }
550 }
551 }
552
553 button.setAction(a);
554 if (group != null) {
555 group.add(button);
556 }
557 if (a != null) {
558 button.addItemListener(a);
559 // JW: move the initial config into the PCL??
560 button.setSelected(a.isSelected());
561 new ToggleActionPropertyChangeListener(a, button);
562 // new ToggleActionPCL(button, a);
563 }
564
565 }
566
567 /**
568 * This method will be called after buttons created from an action. Override
569 * for custom configuration.
570 *
571 * @param button the button to be configured
572 * @param action the action used to construct the menu item.
573 */
574 protected void configureButtonFromExtActionProperties(AbstractButton button, Action action) {
575 if (action.getValue(Action.SHORT_DESCRIPTION) == null) {
576 button.setToolTipText((String)action.getValue(Action.NAME));
577 }
578 // Use the large icon for toolbar buttons.
579 if (action.getValue(AbstractActionExt.LARGE_ICON) != null) {
580 button.setIcon((Icon)action.getValue(AbstractActionExt.LARGE_ICON));
581 }
582 // Don't show the text under the toolbar buttons if they have an icon
583 if (button.getIcon() != null) {
584 button.setText("");
585 }
586 }
587
588
589 /**
590 * This method will be called after menu items are created.
591 * Override for custom configuration.
592 *
593 * @param menuItem the menu item to be configured
594 * @param action the action used to construct the menu item.
595 */
596 protected void configureMenuItemFromExtActionProperties(JMenuItem menuItem, Action action) {
597 }
598
599 /**
600 * Helper method to add a checkbox menu item.
601 */
602 private JCheckBoxMenuItem createCheckBoxMenuItem(AbstractActionExt a) {
603 JCheckBoxMenuItem mi = new JCheckBoxMenuItem();
604 configureSelectableButton(mi, a, null);
605 configureMenuItemFromExtActionProperties(mi, a);
606 return mi;
607 }
608
609 /**
610 * Helper method to add a radio button menu item.
611 */
612 private JRadioButtonMenuItem createRadioButtonMenuItem(ButtonGroup group,
613 AbstractActionExt a) {
614 JRadioButtonMenuItem mi = new JRadioButtonMenuItem();
615 configureSelectableButton(mi, a, group);
616 configureMenuItemFromExtActionProperties(mi, a);
617 return mi;
618 }
619
620
621 }