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&lt;comp&gt;(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    }