/* * $Id$ * * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, * Santa Clara, California 95054, U.S.A. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jdesktop.swingx.action; import java.awt.Insets; import java.beans.PropertyChangeListener; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.swing.AbstractButton; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.ButtonGroup; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JRadioButtonMenuItem; import javax.swing.JToggleButton; import javax.swing.JToolBar; /** * Creates user interface elements based on action ids and lists of action ids. * All action ids must represent actions managed by the ActionManager. * <p> * <h3>Action Lists</h3> * Use the createXXX(List) methods to construct containers of actions like menu * bars, menus, popups and toolbars from actions represented as action ids in a * <i>java.util.List</i>. Each element in the action-list can be one of 3 types: * <ul> * <li>action id: corresponds to an action managed by the ActionManager * <li>null: indicates a separator should be inserted. * <li>java.util.List: represents a submenu. See the note below which describes * the configuration of menus. * </li> * The order of elements in an action-list determines the arrangement of the ui * components which are constructed from the action-list. * <p> * For a menu or submenu, the first element in the action-list represents a menu * and subsequent elements represent menu items or separators (if null). * <p> * This class can be used as a general component factory which will construct * components from Actions if the <code>create<comp>(Action,...)</code> * methods are used. * * @see ActionManager */ public class ActionContainerFactory { /** * Standard margin for toolbar buttons to improve their look */ private static Insets TOOLBAR_BUTTON_MARGIN = new Insets(1, 1, 1, 1); private ActionMap manager; // Map between group id + component and the ButtonGroup private Map<Integer, ButtonGroup> groupMap; /** * Constructs an container factory which uses the default * ActionManager. * */ public ActionContainerFactory() { } /** * Constructs an container factory which uses managed actions. * * @param manager use the actions managed with this manager for * constructing ui componenents. */ public ActionContainerFactory(ActionMap manager) { setActionManager(manager); } /** * Gets the ActionManager instance. If the ActionManager has not been explicitly * set then the default ActionManager instance will be used. * * @return the ActionManager used by the ActionContainerFactory. * @see #setActionManager */ public ActionMap getActionManager() { if (manager == null) { manager = ActionManager.getInstance(); } return manager; } /** * Sets the ActionManager instance that will be used by this * ActionContainerFactory */ public void setActionManager(ActionMap manager) { this.manager = manager; } /** * Constructs a toolbar from an action-list id. By convention, * the identifier of the main toolbar should be "main-toolbar" * * @param list a list of action ids used to construct the toolbar. * @return the toolbar or null */ public JToolBar createToolBar(Object[] list) { return createToolBar(Arrays.asList(list)); } /** * Constructs a toolbar from an action-list id. By convention, * the identifier of the main toolbar should be "main-toolbar" * * @param list a list of action ids used to construct the toolbar. * @return the toolbar or null */ public JToolBar createToolBar(List<?> list) { JToolBar toolbar = new JToolBar(); Iterator<?> iter = list.iterator(); while(iter.hasNext()) { Object element = iter.next(); if (element == null) { toolbar.addSeparator(); } else { AbstractButton button = createButton(element, toolbar); // toolbar buttons shouldn't steal focus button.setFocusable(false); /* * TODO * The next two lines improve the default look of the buttons. * This code should be changed to retrieve the default look * from some UIDefaults object. */ button.setMargin(TOOLBAR_BUTTON_MARGIN); button.setBorderPainted(false); toolbar.add(button); } } return toolbar; } /** * Constructs a popup menu from an array of action ids. * * @param list an array of action ids used to construct the popup. * @return the popup or null */ public JPopupMenu createPopup(Object[] list) { return createPopup(Arrays.asList(list)); } /** * Constructs a popup menu from a list of action ids. * * @param list a list of action ids used to construct the popup. * @return the popup or null */ public JPopupMenu createPopup(List<?> list) { JPopupMenu popup = new JPopupMenu(); Iterator<?> iter = list.iterator(); while(iter.hasNext()) { Object element = iter.next(); if (element == null) { popup.addSeparator(); } else if (element instanceof List<?>) { JMenu newMenu= createMenu((List<?>)element); if (newMenu!= null) { popup.add(newMenu); } } else { popup.add(createMenuItem(element, popup)); } } return popup; } /** * Constructs a menu tree from a list of actions or lists of lists or actions. * * @param actionIds an array which represents the root item. * @return a menu bar which represents the menu bar tree */ public JMenuBar createMenuBar(Object[] actionIds) { return createMenuBar(Arrays.asList(actionIds)); } /** * Constructs a menu tree from a list of actions or lists of lists or actions. * * @param list a list which represents the root item. * @return a menu bar which represents the menu bar tree */ public JMenuBar createMenuBar(List<?> list) { final JMenuBar menubar = new JMenuBar(); for (Object element : list) { if (element == null) { continue; } JMenuItem menu; if (element instanceof Object[]) { menu = createMenu((Object[]) element); } else if (element instanceof List<?>) { menu = createMenu((List<?>) element); } else { menu = createMenuItem(element, menubar); } if (menu != null) { menubar.add(menu); } } return menubar; } /** * Creates and returns a menu from a List which represents actions, separators * and sub-menus. The menu * constructed will have the attributes from the first action in the List. * Subsequent actions in the list represent menu items. * * @param actionIds an array of action ids used to construct the menu and menu items. * the first element represents the action used for the menu, * @return the constructed JMenu or null */ public JMenu createMenu(Object[] actionIds) { return createMenu(Arrays.asList(actionIds)); } /** * Creates and returns a menu from a List which represents actions, separators * and sub-menus. The menu * constructed will have the attributes from the first action in the List. * Subsequent actions in the list represent menu items. * * @param list a list of action ids used to construct the menu and menu items. * the first element represents the action used for the menu, * @return the constructed JMenu or null */ public JMenu createMenu(List<?> list) { // The first item will be the action for the JMenu Action action = getAction(list.get(0)); if (action == null) { return null; } JMenu menu = new JMenu(action); // The rest of the items represent the menu items. for (Object element : list.subList(1, list.size())) { if (element == null) { menu.addSeparator(); } else { JMenuItem newMenu; if (element instanceof Object[]) { newMenu = createMenu((Object[]) element); } else if (element instanceof List<?>) { newMenu = createMenu((List<?>) element); } else { newMenu = createMenuItem(element, menu); } if (newMenu != null) { menu.add(newMenu); } } } return menu; } /** * Convenience method to get the action from an ActionManager. */ private Action getAction(Object id) { return getActionManager().get(id); } /** * Returns the button group corresponding to the groupid * * @param groupid the value of the groupid attribute for the action element * @param container a container which will further identify the ButtonGroup */ private ButtonGroup getGroup(String groupid, JComponent container) { if (groupMap == null) { groupMap = new HashMap<Integer, ButtonGroup>(); } int intCode = groupid.hashCode(); if (container != null) { intCode ^= container.hashCode(); } Integer hashCode = new Integer(intCode); ButtonGroup group = groupMap.get(hashCode); if (group == null) { group = new ButtonGroup(); groupMap.put(hashCode, group); } return group; } /** * Creates a menu item based on the attributes of the action element. * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem * depending on the context of the Action. * * @return a JMenuItem or subclass depending on type. */ private JMenuItem createMenuItem(Object id, JComponent container) { return createMenuItem(getAction(id), container); } /** * Creates a menu item based on the attributes of the action element. * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem * depending on the context of the Action. * * @param action a managed Action * @param container the parent container may be null for non-group actions. * @return a JMenuItem or subclass depending on type. */ private JMenuItem createMenuItem(Action action, JComponent container) { JMenuItem menuItem = null; if (action instanceof AbstractActionExt) { AbstractActionExt ta = (AbstractActionExt)action; if (ta.isStateAction()) { String groupid = (String)ta.getGroup(); if (groupid != null) { // If this action has a groupid attribute then it's a // GroupAction menuItem = createRadioButtonMenuItem(getGroup(groupid, container), (AbstractActionExt)action); } else { menuItem = createCheckBoxMenuItem((AbstractActionExt)action); } } } if (menuItem == null) { menuItem= new JMenuItem(action); configureMenuItemFromExtActionProperties(menuItem, action); } return menuItem; } /** * Creates a menu item based on the attributes of the action. * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem * depending on the context of the Action. * * @param action an action used to create the menu item * @return a JMenuItem or subclass depending on type. */ public JMenuItem createMenuItem(Action action) { return createMenuItem(action, null); } /** * Creates, configures and returns an AbstractButton. * * The attributes of the action element * registered with the ActionManger by the given id. * Will return a JButton or a JToggleButton. * * @param id the identifier * @param container the JComponent which parents the group, if any. * @return an AbstractButton based on the */ public AbstractButton createButton(Object id, JComponent container) { return createButton(getAction(id), container); } /** * Creates a button based on the attributes of the action. If the container * parameter is non-null then it will be used to uniquely identify * the returned component within a ButtonGroup. If the action doesn't * represent a grouped component then this value can be null. * * @param action an action used to create the button * @param container the parent container to uniquely identify * grouped components or null * @return will return a JButton or a JToggleButton. */ public AbstractButton createButton(Action action, JComponent container) { if (action == null) { return null; } AbstractButton button = null; if (action instanceof AbstractActionExt) { // Check to see if we should create a toggle button AbstractActionExt ta = (AbstractActionExt)action; if (ta.isStateAction()) { // If this action has a groupid attribute then it's a // GroupAction String groupid = (String)ta.getGroup(); if (groupid == null) { button = createToggleButton(ta); } else { button = createToggleButton(ta, getGroup(groupid, container)); } } } if (button == null) { // Create a regular button button = new JButton(action); configureButtonFromExtActionProperties(button, action); } return button; } /** * Creates a button based on the attributes of the action. * * @param action an action used to create the button * @return will return a JButton or a JToggleButton. */ public AbstractButton createButton(Action action) { return createButton(action, null); } /** * Adds and configures a toggle button. * @param a an abstraction of a toggle action. */ private JToggleButton createToggleButton(AbstractActionExt a) { return createToggleButton(a, null); } /** * Adds and configures a toggle button. * @param a an abstraction of a toggle action. * @param group the group to add the toggle button or null */ private JToggleButton createToggleButton(AbstractActionExt a, ButtonGroup group) { JToggleButton button = new JToggleButton(); configureButton(button, a, group); return button; } /** * * @param button * @param a * @param group */ public void configureButton(JToggleButton button, AbstractActionExt a, ButtonGroup group) { configureSelectableButton(button, a, group); configureButtonFromExtActionProperties(button, a); } /** * method to configure a "selectable" button from the given AbstractActionExt. * As there is some un-/wiring involved to support synch of the selected property between * the action and the button, all config and unconfig (== setting a null action!) * should be passed through this method. <p> * * It's up to the client to only pass in button's where selected and/or the * group property makes sense. * * PENDING: the group properties are yet untested. * PENDING: think about automated unconfig. * * @param button where selected makes sense * @param a * @param group the button should be added to. * @throws IllegalArgumentException if the given action doesn't have the state flag set. * */ public void configureSelectableButton(AbstractButton button, AbstractActionExt a, ButtonGroup group){ if ((a != null) && !a.isStateAction()) throw new IllegalArgumentException("the Action must be a stateAction"); // we assume that all button configuration is done exclusively through this method!! if (button.getAction() == a) return; // unconfigure if the old Action is a state AbstractActionExt // PENDING JW: automate unconfigure via a PCL that is listening to // the button's action property? Think about memory leak implications! Action oldAction = button.getAction(); if (oldAction instanceof AbstractActionExt) { AbstractActionExt actionExt = (AbstractActionExt) oldAction; // remove as itemListener button.removeItemListener(actionExt); // remove the button related PCL from the old actionExt PropertyChangeListener[] l = actionExt.getPropertyChangeListeners(); for (int i = l.length - 1; i >= 0; i--) { if (l[i] instanceof ToggleActionPropertyChangeListener) { ToggleActionPropertyChangeListener togglePCL = (ToggleActionPropertyChangeListener) l[i]; if (togglePCL.isToggling(button)) { actionExt.removePropertyChangeListener(togglePCL); } } } } button.setAction(a); if (group != null) { group.add(button); } if (a != null) { button.addItemListener(a); // JW: move the initial config into the PCL?? button.setSelected(a.isSelected()); new ToggleActionPropertyChangeListener(a, button); // new ToggleActionPCL(button, a); } } /** * This method will be called after buttons created from an action. Override * for custom configuration. * * @param button the button to be configured * @param action the action used to construct the menu item. */ protected void configureButtonFromExtActionProperties(AbstractButton button, Action action) { if (action.getValue(Action.SHORT_DESCRIPTION) == null) { button.setToolTipText((String)action.getValue(Action.NAME)); } // Use the large icon for toolbar buttons. if (action.getValue(AbstractActionExt.LARGE_ICON) != null) { button.setIcon((Icon)action.getValue(AbstractActionExt.LARGE_ICON)); } // Don't show the text under the toolbar buttons if they have an icon if (button.getIcon() != null) { button.setText(""); } } /** * This method will be called after menu items are created. * Override for custom configuration. * * @param menuItem the menu item to be configured * @param action the action used to construct the menu item. */ protected void configureMenuItemFromExtActionProperties(JMenuItem menuItem, Action action) { } /** * Helper method to add a checkbox menu item. */ private JCheckBoxMenuItem createCheckBoxMenuItem(AbstractActionExt a) { JCheckBoxMenuItem mi = new JCheckBoxMenuItem(); configureSelectableButton(mi, a, null); configureMenuItemFromExtActionProperties(mi, a); return mi; } /** * Helper method to add a radio button menu item. */ private JRadioButtonMenuItem createRadioButtonMenuItem(ButtonGroup group, AbstractActionExt a) { JRadioButtonMenuItem mi = new JRadioButtonMenuItem(); configureSelectableButton(mi, a, group); configureMenuItemFromExtActionProperties(mi, a); return mi; } }