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