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<comp>(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}