001 /*
002 * $Id: ActionContainerFactory.java,v 1.11 2006/05/14 15:55:52 dmouse Exp $
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 managed actions.
085 *
086 * @param manager use the actions managed with this manager for
087 * constructing ui componenents.
088 */
089 public ActionContainerFactory(ActionManager manager) {
090 setActionManager(manager);
091 }
092
093 /**
094 * Gets the ActionManager instance. If the ActionManager has not been explicitly
095 * set then the default ActionManager instance will be used.
096 *
097 * @return the ActionManager used by the ActionContainerFactory.
098 * @see #setActionManager
099 */
100 public ActionManager getActionManager() {
101 if (manager == null) {
102 manager = ActionManager.getInstance();
103 }
104 return manager;
105 }
106
107 /**
108 * Sets the ActionManager instance that will be used by this
109 * ActionContainerFactory
110 */
111 public void setActionManager(ActionManager manager) {
112 ActionManager oldManager = this.manager;
113 if (oldManager != null) {
114 oldManager.setFactory(null);
115 }
116 this.manager = manager;
117
118 if (manager != null) {
119 manager.setFactory(this);
120 }
121 }
122
123 /**
124 * Constructs a toolbar from an action-list id. By convention,
125 * the identifier of the main toolbar should be "main-toolbar"
126 *
127 * @param list a list of action ids used to construct the toolbar.
128 * @return the toolbar or null
129 */
130 public JToolBar createToolBar(Object[] list) {
131 return createToolBar(Arrays.asList(list));
132 }
133
134 /**
135 * Constructs a toolbar from an action-list id. By convention,
136 * the identifier of the main toolbar should be "main-toolbar"
137 *
138 * @param list a list of action ids used to construct the toolbar.
139 * @return the toolbar or null
140 */
141 public JToolBar createToolBar(List list) {
142 JToolBar toolbar = new JToolBar();
143 Iterator iter = list.iterator();
144 while(iter.hasNext()) {
145 Object element = iter.next();
146
147 if (element == null) {
148 toolbar.addSeparator();
149 } else {
150 AbstractButton button = createButton(element, toolbar);
151 // toolbar buttons shouldn't steal focus
152 button.setFocusable(false);
153 /*
154 * TODO
155 * The next two lines improve the default look of the buttons.
156 * This code should be changed to retrieve the default look
157 * from some UIDefaults object.
158 */
159 button.setMargin(TOOLBAR_BUTTON_MARGIN);
160 button.setBorderPainted(false);
161
162 toolbar.add(button);
163 }
164 }
165 return toolbar;
166 }
167
168
169 /**
170 * Constructs a popup menu from an array of action ids.
171 *
172 * @param list an array of action ids used to construct the popup.
173 * @return the popup or null
174 */
175 public JPopupMenu createPopup(Object[] list) {
176 return createPopup(Arrays.asList(list));
177 }
178
179 /**
180 * Constructs a popup menu from a list of action ids.
181 *
182 * @param list a list of action ids used to construct the popup.
183 * @return the popup or null
184 */
185 public JPopupMenu createPopup(List list) {
186 JPopupMenu popup = new JPopupMenu();
187 Iterator iter = list.iterator();
188 while(iter.hasNext()) {
189 Object element = iter.next();
190
191 if (element == null) {
192 popup.addSeparator();
193 } else if (element instanceof List) {
194 JMenu newMenu= createMenu((List)element);
195 if (newMenu!= null) {
196 popup.add(newMenu);
197 }
198 } else {
199 popup.add(createMenuItem(element, popup));
200 }
201 }
202 return popup;
203 }
204
205 /**
206 * Constructs a menu tree from a list of actions or lists of lists or actions.
207 *
208 * TODO This method is broken. It <em>should</em> expect either that every
209 * entry is a List (thus, the sub menus off the main MenuBar), or it should
210 * handle normal actions properly. By submitting a List of all Actions, nothing
211 * is created....
212 * <p>
213 * For example, If my list is [action, action, action], then nothing is added
214 * to the menu bar. However, if my list is [list[action], action, action, action] then
215 * I get a menu and under it the tree actions. This should not be, because if I
216 * wanted those actions to be on the sub menu, then they should have been
217 * listed within the sub list!
218 *
219 * @param actionIds an array which represents the root item.
220 * @return a menu bar which represents the menu bar tree
221 */
222 public JMenuBar createMenuBar(Object[] actionIds) {
223 return createMenuBar(Arrays.asList(actionIds));
224 }
225
226 /**
227 * Constructs a menu tree from a list of actions or lists of lists or actions.
228 * TODO This method is broken. It <em>should</em> expect either that every
229 * entry is a List (thus, the sub menus off the main MenuBar), or it should
230 * handle normal actions properly. By submitting a List of all Actions, nothing
231 * is created....
232 * <p>
233 * For example, If my list is [action, action, action], then nothing is added
234 * to the menu bar. However, if my list is [list[action], action, action, action] then
235 * I get a menu and under it the tree actions. This should not be, because if I
236 * wanted those actions to be on the sub menu, then they should have been
237 * listed within the sub list!
238 *
239 * @param list a list which represents the root item.
240 * @return a menu bar which represents the menu bar tree
241 */
242 public JMenuBar createMenuBar(List list) {
243 JMenuBar menubar = new JMenuBar();
244 JMenu menu = null;
245
246 Iterator iter = list.iterator();
247 while(iter.hasNext()) {
248 Object element = iter.next();
249
250 if (element == null) {
251 if (menu != null) {
252 menu.addSeparator();
253 }
254 } else if (element instanceof List) {
255 menu = createMenu((List)element);
256 if (menu != null) {
257 menubar.add(menu);
258 }
259 } else {
260 if (menu != null) {
261 menu.add(createMenuItem(element, menu));
262 }
263 }
264 }
265 return menubar;
266 }
267
268
269 /**
270 * Creates and returns a menu from a List which represents actions, separators
271 * and sub-menus. The menu
272 * constructed will have the attributes from the first action in the List.
273 * Subsequent actions in the list represent menu items.
274 *
275 * @param actionIds an array of action ids used to construct the menu and menu items.
276 * the first element represents the action used for the menu,
277 * @return the constructed JMenu or null
278 */
279 public JMenu createMenu(Object[] actionIds) {
280 return createMenu(Arrays.asList(actionIds));
281 }
282
283 /**
284 * Creates and returns a menu from a List which represents actions, separators
285 * and sub-menus. The menu
286 * constructed will have the attributes from the first action in the List.
287 * Subsequent actions in the list represent menu items.
288 *
289 * @param list a list of action ids used to construct the menu and menu items.
290 * the first element represents the action used for the menu,
291 * @return the constructed JMenu or null
292 */
293 public JMenu createMenu(List list) {
294 // The first item will be the action for the JMenu
295 Action action = getAction(list.get(0));
296 if (action == null) {
297 return null;
298 }
299 JMenu menu = new JMenu(action);
300
301 // The rest of the items represent the menu items.
302 Iterator iter = list.listIterator(1);
303 while(iter.hasNext()) {
304 Object element = iter.next();
305 if (element == null) {
306 menu.addSeparator();
307 } else if (element instanceof List) {
308 JMenu newMenu = createMenu((List)element);
309 if (newMenu != null) {
310 menu.add(newMenu);
311 }
312 } else {
313 menu.add(createMenuItem(element, menu));
314 }
315 }
316 return menu;
317 }
318
319
320 /**
321 * Convenience method to get the action from an ActionManager.
322 */
323 private Action getAction(Object id) {
324 Action action = getActionManager().getAction(id);
325 if (action == null) {
326 throw new RuntimeException("ERROR: No Action for " + id);
327 }
328 return action;
329 }
330
331 /**
332 * Returns the button group corresponding to the groupid
333 *
334 * @param groupid the value of the groupid attribute for the action element
335 * @param container a container which will further identify the ButtonGroup
336 */
337 private ButtonGroup getGroup(String groupid, JComponent container) {
338 if (groupMap == null) {
339 groupMap = new HashMap();
340 }
341 int intCode = groupid.hashCode();
342 if (container != null) {
343 intCode ^= container.hashCode();
344 }
345 Integer hashCode = new Integer(intCode);
346
347 ButtonGroup group = (ButtonGroup)groupMap.get(hashCode);
348 if (group == null) {
349 group = new ButtonGroup();
350 groupMap.put(hashCode, group);
351 }
352 return group;
353 }
354
355 /**
356 * Creates a menu item based on the attributes of the action element.
357 * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem
358 * depending on the context of the Action.
359 *
360 * @return a JMenuItem or subclass depending on type.
361 */
362 private JMenuItem createMenuItem(Object id, JComponent container) {
363 return createMenuItem(getAction(id), container);
364 }
365
366
367 /**
368 * Creates a menu item based on the attributes of the action element.
369 * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem
370 * depending on the context of the Action.
371 *
372 * @param action a mangaged Action
373 * @param container the parent container may be null for non-group actions.
374 * @return a JMenuItem or subclass depending on type.
375 */
376 public JMenuItem createMenuItem(Action action, JComponent container) {
377 JMenuItem menuItem = null;
378 if (action instanceof AbstractActionExt) {
379 AbstractActionExt ta = (AbstractActionExt)action;
380
381 if (ta.isStateAction()) {
382 String groupid = (String)ta.getGroup();
383 if (groupid != null) {
384 // If this action has a groupid attribute then it's a
385 // GroupAction
386 menuItem = createRadioButtonMenuItem(getGroup(groupid, container),
387 (AbstractActionExt)action);
388 } else {
389 menuItem = createCheckBoxMenuItem((AbstractActionExt)action);
390 }
391 }
392 }
393
394 if (menuItem == null) {
395 menuItem= new JMenuItem(action);
396 configureMenuItemFromExtActionProperties(menuItem, action);
397 }
398 return menuItem;
399 }
400
401 /**
402 * Creates a menu item based on the attributes of the action.
403 * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem
404 * depending on the context of the Action.
405 *
406 * @param action an action used to create the menu item
407 * @return a JMenuItem or subclass depending on type.
408 */
409 public JMenuItem createMenuItem(Action action) {
410 return createMenuItem(action, null);
411 }
412
413
414 /**
415 * Creates a button based on the attributes of the action element.
416 * Will return a JButton or a JToggleButton.
417 */
418 private AbstractButton createButton(Object id, JComponent container) {
419 return createButton(getAction(id), container);
420 }
421
422 /**
423 * Creates a button based on the attributes of the action. If the container
424 * parameter is non-null then it will be used to uniquely identify
425 * the returned component within a ButtonGroup. If the action doesn't
426 * represent a grouped component then this value can be null.
427 *
428 * @param action an action used to create the button
429 * @param container the parent container to uniquely identify
430 * grouped components or null
431 * @return will return a JButton or a JToggleButton.
432 */
433 public AbstractButton createButton(Action action, JComponent container) {
434 if (action == null) {
435 return null;
436 }
437
438 AbstractButton button = null;
439 if (action instanceof AbstractActionExt) {
440 // Check to see if we should create a toggle button
441 AbstractActionExt ta = (AbstractActionExt)action;
442
443 if (ta.isStateAction()) {
444 // If this action has a groupid attribute then it's a
445 // GroupAction
446 String groupid = (String)ta.getGroup();
447 if (groupid == null) {
448 button = createToggleButton(ta);
449 } else {
450 button = createToggleButton(ta, getGroup(groupid, container));
451 }
452 }
453 }
454
455 if (button == null) {
456 // Create a regular button
457 button = new JButton(action);
458 configureButtonFromExtActionProperties(button, action);
459 }
460 return button;
461 }
462
463 /**
464 * Creates a button based on the attributes of the action.
465 *
466 * @param action an action used to create the button
467 * @return will return a JButton or a JToggleButton.
468 */
469 public AbstractButton createButton(Action action) {
470 return createButton(action, null);
471 }
472
473 /**
474 * Adds and configures a toggle button.
475 * @param a an abstraction of a toggle action.
476 */
477 private JToggleButton createToggleButton(AbstractActionExt a) {
478 return createToggleButton(a, null);
479 }
480
481 /**
482 * Adds and configures a toggle button.
483 * @param a an abstraction of a toggle action.
484 * @param group the group to add the toggle button or null
485 */
486 private JToggleButton createToggleButton(AbstractActionExt a, ButtonGroup group) {
487 JToggleButton button = new JToggleButton();
488 configureButton(button, a, group);
489 return button;
490 }
491
492 /**
493 *
494 * @param button
495 * @param a
496 * @param group
497 */
498 public void configureButton(JToggleButton button, AbstractActionExt a, ButtonGroup group) {
499 configureSelectableButton(button, a, group);
500 configureButtonFromExtActionProperties(button, a);
501 }
502
503 /**
504 * method to configure a "selectable" button from the given AbstractActionExt.
505 * As there is some un-/wiring involved to support synch of the selected property between
506 * the action and the button, all config and unconfig (== setting a null action!)
507 * should be passed through this method. <p>
508 *
509 * It's up to the client to only pass in button's where selected and/or the
510 * group property makes sense.
511 *
512 * PENDING: the group properties are yet untested.
513 * PENDING: think about automated unconfig.
514 *
515 * @param button where selected makes sense
516 * @param a
517 * @param group the button should be added to.
518 * @throws IllegalArgumentException if the given action doesn't have the state flag set.
519 *
520 */
521 public void configureSelectableButton(AbstractButton button, AbstractActionExt a, ButtonGroup group){
522 if ((a != null) && !a.isStateAction()) throw
523 new IllegalArgumentException("the Action must be a stateAction");
524 // we assume that all button configuration is done exclusively through this method!!
525 if (button.getAction() == a) return;
526
527 // unconfigure if the old Action is a state AbstractActionExt
528 // PENDING JW: automate unconfigure via a PCL that is listening to
529 // the button's action property? Think about memory leak implications!
530 Action oldAction = button.getAction();
531 if (oldAction instanceof AbstractActionExt) {
532 AbstractActionExt actionExt = (AbstractActionExt) oldAction;
533 // remove as itemListener
534 button.removeItemListener(actionExt);
535 // remove the button related PCL from the old actionExt
536 PropertyChangeListener[] l = actionExt.getPropertyChangeListeners();
537 for (int i = l.length - 1; i >= 0; i--) {
538 if (l[i] instanceof ToggleActionPropertyChangeListener) {
539 ToggleActionPropertyChangeListener togglePCL = (ToggleActionPropertyChangeListener) l[i];
540 if (togglePCL.isToggling(button)) {
541 actionExt.removePropertyChangeListener(togglePCL);
542 }
543 }
544 }
545 }
546
547 button.setAction(a);
548 if (group != null) {
549 group.add(button);
550 }
551 if (a != null) {
552 button.addItemListener(a);
553 // JW: move the initial config into the PCL??
554 button.setSelected(a.isSelected());
555 new ToggleActionPropertyChangeListener(a, button);
556 // new ToggleActionPCL(button, a);
557 }
558
559 }
560
561 /**
562 * This method will be called after buttons created from an action. Override
563 * for custom configuration.
564 *
565 * @param button the button to be configured
566 * @param action the action used to construct the menu item.
567 */
568 protected void configureButtonFromExtActionProperties(AbstractButton button, Action action) {
569 if (action.getValue(Action.SHORT_DESCRIPTION) == null) {
570 button.setToolTipText((String)action.getValue(Action.NAME));
571 }
572 // Use the large icon for toolbar buttons.
573 if (action.getValue(AbstractActionExt.LARGE_ICON) != null) {
574 button.setIcon((Icon)action.getValue(AbstractActionExt.LARGE_ICON));
575 }
576 // Don't show the text under the toolbar buttons if they have an icon
577 if (button.getIcon() != null) {
578 button.setText("");
579 }
580 }
581
582
583 /**
584 * This method will be called after menu items are created.
585 * Override for custom configuration.
586 *
587 * @param menuItem the menu item to be configured
588 * @param action the action used to construct the menu item.
589 */
590 protected void configureMenuItemFromExtActionProperties(JMenuItem menuItem, Action action) {
591 }
592
593 /**
594 * Helper method to add a checkbox menu item.
595 */
596 private JCheckBoxMenuItem createCheckBoxMenuItem(AbstractActionExt a) {
597 JCheckBoxMenuItem mi = new JCheckBoxMenuItem();
598 configureSelectableButton(mi, a, null);
599 configureMenuItemFromExtActionProperties(mi, a);
600 return mi;
601 }
602
603 /**
604 * Helper method to add a radio button menu item.
605 */
606 private JRadioButtonMenuItem createRadioButtonMenuItem(ButtonGroup group,
607 AbstractActionExt a) {
608 JRadioButtonMenuItem mi = new JRadioButtonMenuItem();
609 configureSelectableButton(mi, a, group);
610 configureMenuItemFromExtActionProperties(mi, a);
611 return mi;
612 }
613
614
615 }