001/*
002 * $Id: ActionContainerFactory.java 3980 2011-03-28 20:24:46Z kschaefe $
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 */
021package org.jdesktop.swingx.action;
022
023import java.awt.Insets;
024import java.beans.PropertyChangeListener;
025import java.util.Arrays;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Map;
030
031import javax.swing.AbstractButton;
032import javax.swing.Action;
033import javax.swing.ActionMap;
034import javax.swing.ButtonGroup;
035import javax.swing.Icon;
036import javax.swing.JButton;
037import javax.swing.JCheckBoxMenuItem;
038import javax.swing.JComponent;
039import javax.swing.JMenu;
040import javax.swing.JMenuBar;
041import javax.swing.JMenuItem;
042import javax.swing.JPopupMenu;
043import javax.swing.JRadioButtonMenuItem;
044import javax.swing.JToggleButton;
045import javax.swing.JToolBar;
046
047/**
048 * Creates user interface elements based on action ids and lists of action ids.
049 * All action ids must represent actions managed by the ActionManager.
050 * <p>
051 * <h3>Action Lists</h3>
052 * Use the createXXX(List) methods to construct containers of actions like menu 
053 * bars, menus, popups and toolbars from actions represented as action ids in a 
054 * <i>java.util.List</i>. Each element in the action-list can be one of 3 types:
055 * <ul>
056 * <li>action id: corresponds to an action managed by the ActionManager
057 * <li>null: indicates a separator should be inserted.
058 * <li>java.util.List: represents a submenu. See the note below which describes 
059 * the configuration of menus. 
060 * </li>
061 * The order of elements in an action-list determines the arrangement of the ui 
062 * components which are constructed from the action-list.
063 * <p>
064 * For a menu or submenu, the first element in the action-list represents a menu 
065 * and subsequent elements represent menu items or separators (if null). 
066 * <p>
067 * This class can be used as a general component factory which will construct
068 * components from Actions if the <code>create&lt;comp&gt;(Action,...)</code>
069 * methods are used.
070 *
071 * @see ActionManager
072 */
073public class ActionContainerFactory {
074    /**
075     * Standard margin for toolbar buttons to improve their look
076     */
077    private static Insets TOOLBAR_BUTTON_MARGIN = new Insets(1, 1, 1, 1);
078    
079    private ActionMap manager;
080
081    // Map between group id + component and the ButtonGroup
082    private Map<Integer, ButtonGroup> groupMap;
083
084    /**
085     * Constructs an container factory which uses the default 
086     * ActionManager.
087     *
088     */
089    public ActionContainerFactory() {
090    }
091    /**
092     * Constructs an container factory which uses managed actions.
093     *
094     * @param manager use the actions managed with this manager for
095     *                constructing ui componenents.
096     */
097    public ActionContainerFactory(ActionMap manager) {
098        setActionManager(manager);
099    }
100
101    /**
102     * Gets the ActionManager instance. If the ActionManager has not been explicitly
103     * set then the default ActionManager instance will be used.
104     *
105     * @return the ActionManager used by the ActionContainerFactory.
106     * @see #setActionManager
107     */
108    public ActionMap getActionManager() {
109        if (manager == null) {
110            manager = ActionManager.getInstance();
111        }
112        return manager;
113    }
114
115    /**
116     * Sets the ActionManager instance that will be used by this
117     * ActionContainerFactory
118     */
119    public void setActionManager(ActionMap manager) {
120        this.manager = manager;
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     * @param actionIds an array which represents the root item.
209     * @return a menu bar which represents the menu bar tree
210     */
211    public JMenuBar createMenuBar(Object[] actionIds) {
212        return createMenuBar(Arrays.asList(actionIds));
213    }
214
215    /**
216     * Constructs a menu tree from a list of actions or lists of lists or actions.
217     *
218     * @param list a list which represents the root item.
219     * @return a menu bar which represents the menu bar tree
220     */
221    public JMenuBar createMenuBar(List<?> list) {
222        final JMenuBar menubar = new JMenuBar();
223
224        for (Object element : list) {
225            if (element == null) {
226                continue;
227            }
228
229            JMenuItem menu;
230
231            if (element instanceof Object[]) {
232                menu = createMenu((Object[]) element);
233            } else if (element instanceof List<?>) {
234                menu = createMenu((List<?>) element);
235            } else {
236                menu = createMenuItem(element, menubar);
237            }
238
239            if (menu != null) {
240                menubar.add(menu);
241            }
242        }
243        
244        return menubar;
245    }
246
247
248    /**
249     * Creates and returns a menu from a List which represents actions, separators
250     * and sub-menus. The menu
251     * constructed will have the attributes from the first action in the List.
252     * Subsequent actions in the list represent menu items.
253     *
254     * @param actionIds an array of action ids used to construct the menu and menu items.
255     *             the first element represents the action used for the menu,
256     * @return the constructed JMenu or null
257     */
258     public JMenu createMenu(Object[] actionIds) {
259        return createMenu(Arrays.asList(actionIds));
260    }
261
262    /**
263     * Creates and returns a menu from a List which represents actions, separators
264     * and sub-menus. The menu
265     * constructed will have the attributes from the first action in the List.
266     * Subsequent actions in the list represent menu items.
267     *
268     * @param list a list of action ids used to construct the menu and menu items.
269     *             the first element represents the action used for the menu,
270     * @return the constructed JMenu or null
271     */
272    public JMenu createMenu(List<?> list) {
273        // The first item will be the action for the JMenu
274        Action action = getAction(list.get(0));
275        
276        if (action == null) {
277            return null;
278        }
279        
280        JMenu menu = new JMenu(action);
281
282        // The rest of the items represent the menu items.
283        for (Object element : list.subList(1, list.size())) {
284            if (element == null) {
285                menu.addSeparator();
286            } else {
287                JMenuItem newMenu;
288
289                if (element instanceof Object[]) {
290                    newMenu = createMenu((Object[]) element);
291                } else if (element instanceof List<?>) {
292                    newMenu = createMenu((List<?>) element);
293                } else {
294                    newMenu = createMenuItem(element, menu);
295                }
296
297                if (newMenu != null) {
298                    menu.add(newMenu);
299                }
300            }
301        }
302        
303        return menu;
304    }
305
306    /**
307     * Convenience method to get the action from an ActionManager.
308     */
309    private Action getAction(Object id) {
310        return getActionManager().get(id);
311    }
312
313    /**
314     * Returns the button group corresponding to the groupid
315     *
316     * @param groupid the value of the groupid attribute for the action element
317     * @param container a container which will further identify the ButtonGroup
318     */
319    private ButtonGroup getGroup(String groupid, JComponent container) {
320        if (groupMap == null) {
321            groupMap = new HashMap<Integer, ButtonGroup>();
322        }
323        int intCode = groupid.hashCode();
324        if (container != null) {
325            intCode ^= container.hashCode();
326        }
327        Integer hashCode = new Integer(intCode);
328
329        ButtonGroup group = groupMap.get(hashCode);
330        if (group == null) {
331            group = new ButtonGroup();
332            groupMap.put(hashCode, group);
333        }
334        return group;
335    }
336
337    /**
338     * Creates a menu item based on the attributes of the action element.
339     * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem
340     * depending on the context of the Action.
341     *
342     * @return a JMenuItem or subclass depending on type.
343     */
344    private JMenuItem createMenuItem(Object id, JComponent container) {
345        return createMenuItem(getAction(id), container);
346    }
347
348
349    /**
350     * Creates a menu item based on the attributes of the action element.
351     * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem
352     * depending on the context of the Action.
353     *
354     * @param action a managed Action
355     * @param container the parent container may be null for non-group actions.
356     * @return a JMenuItem or subclass depending on type.
357     */
358    private JMenuItem createMenuItem(Action action, JComponent container) {
359        JMenuItem menuItem = null;
360        if (action instanceof AbstractActionExt) {
361            AbstractActionExt ta = (AbstractActionExt)action;
362
363            if (ta.isStateAction()) {
364                String groupid = (String)ta.getGroup();
365                if (groupid != null) {
366                    // If this action has a groupid attribute then it's a
367                    // GroupAction
368                    menuItem = createRadioButtonMenuItem(getGroup(groupid, container),
369                                                         (AbstractActionExt)action);
370                } else {
371                    menuItem = createCheckBoxMenuItem((AbstractActionExt)action);
372                }
373            }
374        }
375
376        if (menuItem == null) {
377            menuItem= new JMenuItem(action);
378            configureMenuItemFromExtActionProperties(menuItem, action);
379        }
380        return menuItem;
381    }
382
383    /**
384     * Creates a menu item based on the attributes of the action.
385     * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem
386     * depending on the context of the Action.
387     *
388     * @param action an action used to create the menu item
389     * @return a JMenuItem or subclass depending on type.
390     */
391    public JMenuItem createMenuItem(Action action) {
392        return createMenuItem(action, null);
393    }
394
395
396    /**
397     * Creates, configures and returns an AbstractButton. 
398     * 
399     * The attributes of the action element 
400     * registered with the ActionManger by the given id.
401     * Will return a JButton or a JToggleButton.
402     * 
403     * @param id the identifier 
404     * @param container the JComponent which parents the group, if any.
405     * @return an AbstractButton based on the 
406     */
407    public AbstractButton createButton(Object id, JComponent container) {
408        return createButton(getAction(id), container);
409    }
410
411    /**
412     * Creates a button based on the attributes of the action. If the container
413     * parameter is non-null then it will be used to uniquely identify
414     * the returned component within a ButtonGroup. If the action doesn't
415     * represent a grouped component then this value can be null.
416     *
417     * @param action an action used to create the button
418     * @param container the parent container to uniquely identify
419     *        grouped components or null
420     * @return will return a JButton or a JToggleButton.
421     */
422    public AbstractButton createButton(Action action, JComponent container) {
423        if (action == null) {
424            return null;
425        }
426
427        AbstractButton button = null;
428        if (action instanceof AbstractActionExt) {
429            // Check to see if we should create a toggle button
430            AbstractActionExt ta = (AbstractActionExt)action;
431
432            if (ta.isStateAction()) {
433                // If this action has a groupid attribute then it's a
434                // GroupAction
435                String groupid = (String)ta.getGroup();
436                if (groupid == null) {
437                    button = createToggleButton(ta);
438                } else {
439                    button = createToggleButton(ta, getGroup(groupid, container));
440                }
441            }
442        }
443
444        if (button == null) {
445            // Create a regular button
446            button = new JButton(action);
447            configureButtonFromExtActionProperties(button, action);
448        }
449        return button;
450    }
451
452    /**
453     * Creates a button based on the attributes of the action.
454     *
455     * @param action an action used to create the button
456     * @return will return a JButton or a JToggleButton.
457     */
458    public AbstractButton createButton(Action action)  {
459        return createButton(action, null);
460    }
461
462    /**
463     * Adds and configures a toggle button.
464     * @param a an abstraction of a toggle action.
465     */
466    private JToggleButton createToggleButton(AbstractActionExt a)  {
467        return createToggleButton(a, null);
468    }
469
470    /**
471     * Adds and configures a toggle button.
472     * @param a an abstraction of a toggle action.
473     * @param group the group to add the toggle button or null
474     */
475    private JToggleButton createToggleButton(AbstractActionExt a, ButtonGroup group)  {
476        JToggleButton button = new JToggleButton();
477        configureButton(button, a, group);
478        return button;
479    }
480
481    /**
482     * 
483     * @param button
484     * @param a
485     * @param group
486     */
487    public void configureButton(JToggleButton button, AbstractActionExt a, ButtonGroup group) {
488       configureSelectableButton(button, a, group);
489       configureButtonFromExtActionProperties(button, a);
490    }
491
492    /**
493     * method to configure a "selectable" button from the given AbstractActionExt.
494     * As there is some un-/wiring involved to support synch of the selected property between
495     * the action and the button, all config and unconfig (== setting a null action!) 
496     * should be passed through this method. <p>
497     * 
498     * It's up to the client to only pass in button's where selected and/or the 
499     * group property makes sense. 
500     * 
501     * PENDING: the group properties are yet untested.
502     * PENDING: think about automated unconfig.
503     * 
504     * @param button where selected makes sense
505     * @param a
506     * @param group the button should be added to.
507     * @throws IllegalArgumentException if the given action doesn't have the state flag set. 
508     * 
509     */
510    public void configureSelectableButton(AbstractButton button, AbstractActionExt a, ButtonGroup group){
511        if ((a != null) && !a.isStateAction()) throw
512            new IllegalArgumentException("the Action must be a stateAction");
513        // we assume that all button configuration is done exclusively through this method!!
514        if (button.getAction() == a) return;
515
516        // unconfigure if the old Action is a state AbstractActionExt
517        // PENDING JW: automate unconfigure via a PCL that is listening to  
518        // the button's action property? Think about memory leak implications!
519        Action oldAction = button.getAction();
520        if (oldAction instanceof AbstractActionExt) {
521            AbstractActionExt actionExt = (AbstractActionExt) oldAction;
522            // remove as itemListener
523            button.removeItemListener(actionExt);
524            // remove the button related PCL from the old actionExt
525            PropertyChangeListener[] l = actionExt.getPropertyChangeListeners();
526            for (int i = l.length - 1; i >= 0; i--) {
527                if (l[i] instanceof ToggleActionPropertyChangeListener) {
528                    ToggleActionPropertyChangeListener togglePCL = (ToggleActionPropertyChangeListener) l[i];
529                    if (togglePCL.isToggling(button)) {
530                        actionExt.removePropertyChangeListener(togglePCL);
531                    }
532                }
533            }
534        }
535        
536        button.setAction(a);
537        if (group != null) {
538            group.add(button);
539        }
540        if (a != null) {
541            button.addItemListener(a);
542            // JW: move the initial config into the PCL??
543            button.setSelected(a.isSelected());
544            new ToggleActionPropertyChangeListener(a, button);
545//          new ToggleActionPCL(button, a);
546        } 
547        
548    }
549
550    /**
551     * This method will be called after buttons created from an action. Override
552     * for custom configuration.
553     * 
554     * @param button the button to be configured
555     * @param action the action used to construct the menu item.
556     */
557    protected void configureButtonFromExtActionProperties(AbstractButton button, Action action)  {
558        if (action.getValue(Action.SHORT_DESCRIPTION) == null) {
559            button.setToolTipText((String)action.getValue(Action.NAME));
560        }
561        // Use the large icon for toolbar buttons.
562        if (action.getValue(AbstractActionExt.LARGE_ICON) != null) {
563            button.setIcon((Icon)action.getValue(AbstractActionExt.LARGE_ICON));
564        }
565        // Don't show the text under the toolbar buttons if they have an icon
566        if (button.getIcon() != null) {
567            button.setText("");
568        }
569    }
570
571
572    /**
573     * This method will be called after menu items are created.
574     * Override for custom configuration.
575     *
576     * @param menuItem the menu item to be configured
577     * @param action the action used to construct the menu item.
578     */
579    protected void configureMenuItemFromExtActionProperties(JMenuItem menuItem, Action action) {
580    }
581
582    /**
583     * Helper method to add a checkbox menu item.
584     */
585    private JCheckBoxMenuItem createCheckBoxMenuItem(AbstractActionExt a) {
586        JCheckBoxMenuItem mi = new JCheckBoxMenuItem();
587        configureSelectableButton(mi, a, null);
588        configureMenuItemFromExtActionProperties(mi, a);
589        return mi;
590    }
591
592    /**
593     * Helper method to add a radio button menu item.
594     */
595    private JRadioButtonMenuItem createRadioButtonMenuItem(ButtonGroup group,
596                                                                  AbstractActionExt a)  {
597        JRadioButtonMenuItem mi = new JRadioButtonMenuItem();
598        configureSelectableButton(mi, a, group);
599        configureMenuItemFromExtActionProperties(mi, a);
600        return mi;
601    }
602    
603    
604}