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&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 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    }