001 /* 002 * $Id: ActionContainerFactory.java 1265 2006-07-21 17:15:39Z kleopatra $ 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 the default 085 * ActionManager. 086 * 087 */ 088 public ActionContainerFactory() { 089 } 090 /** 091 * Constructs an container factory which uses managed actions. 092 * 093 * @param manager use the actions managed with this manager for 094 * constructing ui componenents. 095 */ 096 public ActionContainerFactory(ActionManager manager) { 097 setActionManager(manager); 098 } 099 100 /** 101 * Gets the ActionManager instance. If the ActionManager has not been explicitly 102 * set then the default ActionManager instance will be used. 103 * 104 * @return the ActionManager used by the ActionContainerFactory. 105 * @see #setActionManager 106 */ 107 public ActionManager getActionManager() { 108 if (manager == null) { 109 manager = ActionManager.getInstance(); 110 } 111 return manager; 112 } 113 114 /** 115 * Sets the ActionManager instance that will be used by this 116 * ActionContainerFactory 117 */ 118 public void setActionManager(ActionManager manager) { 119 this.manager = manager; 120 } 121 122 /** 123 * Constructs a toolbar from an action-list id. By convention, 124 * the identifier of the main toolbar should be "main-toolbar" 125 * 126 * @param list a list of action ids used to construct the toolbar. 127 * @return the toolbar or null 128 */ 129 public JToolBar createToolBar(Object[] list) { 130 return createToolBar(Arrays.asList(list)); 131 } 132 133 /** 134 * Constructs a toolbar from an action-list id. By convention, 135 * the identifier of the main toolbar should be "main-toolbar" 136 * 137 * @param list a list of action ids used to construct the toolbar. 138 * @return the toolbar or null 139 */ 140 public JToolBar createToolBar(List list) { 141 JToolBar toolbar = new JToolBar(); 142 Iterator iter = list.iterator(); 143 while(iter.hasNext()) { 144 Object element = iter.next(); 145 146 if (element == null) { 147 toolbar.addSeparator(); 148 } else { 149 AbstractButton button = createButton(element, toolbar); 150 // toolbar buttons shouldn't steal focus 151 button.setFocusable(false); 152 /* 153 * TODO 154 * The next two lines improve the default look of the buttons. 155 * This code should be changed to retrieve the default look 156 * from some UIDefaults object. 157 */ 158 button.setMargin(TOOLBAR_BUTTON_MARGIN); 159 button.setBorderPainted(false); 160 161 toolbar.add(button); 162 } 163 } 164 return toolbar; 165 } 166 167 168 /** 169 * Constructs a popup menu from an array of action ids. 170 * 171 * @param list an array of action ids used to construct the popup. 172 * @return the popup or null 173 */ 174 public JPopupMenu createPopup(Object[] list) { 175 return createPopup(Arrays.asList(list)); 176 } 177 178 /** 179 * Constructs a popup menu from a list of action ids. 180 * 181 * @param list a list of action ids used to construct the popup. 182 * @return the popup or null 183 */ 184 public JPopupMenu createPopup(List list) { 185 JPopupMenu popup = new JPopupMenu(); 186 Iterator iter = list.iterator(); 187 while(iter.hasNext()) { 188 Object element = iter.next(); 189 190 if (element == null) { 191 popup.addSeparator(); 192 } else if (element instanceof List) { 193 JMenu newMenu= createMenu((List)element); 194 if (newMenu!= null) { 195 popup.add(newMenu); 196 } 197 } else { 198 popup.add(createMenuItem(element, popup)); 199 } 200 } 201 return popup; 202 } 203 204 /** 205 * Constructs a menu tree from a list of actions or lists of lists or actions. 206 * 207 * TODO This method is broken. It <em>should</em> expect either that every 208 * entry is a List (thus, the sub menus off the main MenuBar), or it should 209 * handle normal actions properly. By submitting a List of all Actions, nothing 210 * is created.... 211 * <p> 212 * For example, If my list is [action, action, action], then nothing is added 213 * to the menu bar. However, if my list is [list[action], action, action, action] then 214 * I get a menu and under it the tree actions. This should not be, because if I 215 * wanted those actions to be on the sub menu, then they should have been 216 * listed within the sub list! 217 * 218 * @param actionIds an array which represents the root item. 219 * @return a menu bar which represents the menu bar tree 220 */ 221 public JMenuBar createMenuBar(Object[] actionIds) { 222 return createMenuBar(Arrays.asList(actionIds)); 223 } 224 225 /** 226 * Constructs a menu tree from a list of actions or lists of lists or actions. 227 * TODO This method is broken. It <em>should</em> expect either that every 228 * entry is a List (thus, the sub menus off the main MenuBar), or it should 229 * handle normal actions properly. By submitting a List of all Actions, nothing 230 * is created.... 231 * <p> 232 * For example, If my list is [action, action, action], then nothing is added 233 * to the menu bar. However, if my list is [list[action], action, action, action] then 234 * I get a menu and under it the tree actions. This should not be, because if I 235 * wanted those actions to be on the sub menu, then they should have been 236 * listed within the sub list! 237 * 238 * @param list a list which represents the root item. 239 * @return a menu bar which represents the menu bar tree 240 */ 241 public JMenuBar createMenuBar(List list) { 242 JMenuBar menubar = new JMenuBar(); 243 JMenu menu = null; 244 245 Iterator iter = list.iterator(); 246 while(iter.hasNext()) { 247 Object element = iter.next(); 248 249 if (element == null) { 250 if (menu != null) { 251 menu.addSeparator(); 252 } 253 } else if (element instanceof List) { 254 menu = createMenu((List)element); 255 if (menu != null) { 256 menubar.add(menu); 257 } 258 } else { 259 if (menu != null) { 260 menu.add(createMenuItem(element, menu)); 261 } 262 } 263 } 264 return menubar; 265 } 266 267 268 /** 269 * Creates and returns a menu from a List which represents actions, separators 270 * and sub-menus. The menu 271 * constructed will have the attributes from the first action in the List. 272 * Subsequent actions in the list represent menu items. 273 * 274 * @param actionIds an array of action ids used to construct the menu and menu items. 275 * the first element represents the action used for the menu, 276 * @return the constructed JMenu or null 277 */ 278 public JMenu createMenu(Object[] actionIds) { 279 return createMenu(Arrays.asList(actionIds)); 280 } 281 282 /** 283 * Creates and returns a menu from a List which represents actions, separators 284 * and sub-menus. The menu 285 * constructed will have the attributes from the first action in the List. 286 * Subsequent actions in the list represent menu items. 287 * 288 * @param list a list of action ids used to construct the menu and menu items. 289 * the first element represents the action used for the menu, 290 * @return the constructed JMenu or null 291 */ 292 public JMenu createMenu(List list) { 293 // The first item will be the action for the JMenu 294 Action action = getAction(list.get(0)); 295 if (action == null) { 296 return null; 297 } 298 JMenu menu = new JMenu(action); 299 300 // The rest of the items represent the menu items. 301 Iterator iter = list.listIterator(1); 302 while(iter.hasNext()) { 303 Object element = iter.next(); 304 if (element == null) { 305 menu.addSeparator(); 306 } else if (element instanceof List) { 307 JMenu newMenu = createMenu((List)element); 308 if (newMenu != null) { 309 menu.add(newMenu); 310 } 311 } else { 312 menu.add(createMenuItem(element, menu)); 313 } 314 } 315 return menu; 316 } 317 318 319 /** 320 * Convenience method to get the action from an ActionManager. 321 */ 322 private Action getAction(Object id) { 323 Action action = getActionManager().getAction(id); 324 if (action == null) { 325 throw new RuntimeException("ERROR: No Action for " + id); 326 } 327 return action; 328 } 329 330 /** 331 * Returns the button group corresponding to the groupid 332 * 333 * @param groupid the value of the groupid attribute for the action element 334 * @param container a container which will further identify the ButtonGroup 335 */ 336 private ButtonGroup getGroup(String groupid, JComponent container) { 337 if (groupMap == null) { 338 groupMap = new HashMap(); 339 } 340 int intCode = groupid.hashCode(); 341 if (container != null) { 342 intCode ^= container.hashCode(); 343 } 344 Integer hashCode = new Integer(intCode); 345 346 ButtonGroup group = (ButtonGroup)groupMap.get(hashCode); 347 if (group == null) { 348 group = new ButtonGroup(); 349 groupMap.put(hashCode, group); 350 } 351 return group; 352 } 353 354 /** 355 * Creates a menu item based on the attributes of the action element. 356 * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem 357 * depending on the context of the Action. 358 * 359 * @return a JMenuItem or subclass depending on type. 360 */ 361 private JMenuItem createMenuItem(Object id, JComponent container) { 362 return createMenuItem(getAction(id), container); 363 } 364 365 366 /** 367 * Creates a menu item based on the attributes of the action element. 368 * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem 369 * depending on the context of the Action. 370 * 371 * @param action a mangaged Action 372 * @param container the parent container may be null for non-group actions. 373 * @return a JMenuItem or subclass depending on type. 374 */ 375 public JMenuItem createMenuItem(Action action, JComponent container) { 376 JMenuItem menuItem = null; 377 if (action instanceof AbstractActionExt) { 378 AbstractActionExt ta = (AbstractActionExt)action; 379 380 if (ta.isStateAction()) { 381 String groupid = (String)ta.getGroup(); 382 if (groupid != null) { 383 // If this action has a groupid attribute then it's a 384 // GroupAction 385 menuItem = createRadioButtonMenuItem(getGroup(groupid, container), 386 (AbstractActionExt)action); 387 } else { 388 menuItem = createCheckBoxMenuItem((AbstractActionExt)action); 389 } 390 } 391 } 392 393 if (menuItem == null) { 394 menuItem= new JMenuItem(action); 395 configureMenuItemFromExtActionProperties(menuItem, action); 396 } 397 return menuItem; 398 } 399 400 /** 401 * Creates a menu item based on the attributes of the action. 402 * Will return a JMenuItem, JRadioButtonMenuItem or a JCheckBoxMenuItem 403 * depending on the context of the Action. 404 * 405 * @param action an action used to create the menu item 406 * @return a JMenuItem or subclass depending on type. 407 */ 408 public JMenuItem createMenuItem(Action action) { 409 return createMenuItem(action, null); 410 } 411 412 413 /** 414 * Creates, configures and returns an AbstractButton. 415 * 416 * The attributes of the action element 417 * registered with the ActionManger by the given id. 418 * Will return a JButton or a JToggleButton. 419 * 420 * @param id the identifer 421 * @param container the JComponent which parents the group, if any. 422 * @return an AbstractButton based on the 423 */ 424 public AbstractButton createButton(Object id, JComponent container) { 425 return createButton(getAction(id), container); 426 } 427 428 /** 429 * Creates a button based on the attributes of the action. If the container 430 * parameter is non-null then it will be used to uniquely identify 431 * the returned component within a ButtonGroup. If the action doesn't 432 * represent a grouped component then this value can be null. 433 * 434 * @param action an action used to create the button 435 * @param container the parent container to uniquely identify 436 * grouped components or null 437 * @return will return a JButton or a JToggleButton. 438 */ 439 public AbstractButton createButton(Action action, JComponent container) { 440 if (action == null) { 441 return null; 442 } 443 444 AbstractButton button = null; 445 if (action instanceof AbstractActionExt) { 446 // Check to see if we should create a toggle button 447 AbstractActionExt ta = (AbstractActionExt)action; 448 449 if (ta.isStateAction()) { 450 // If this action has a groupid attribute then it's a 451 // GroupAction 452 String groupid = (String)ta.getGroup(); 453 if (groupid == null) { 454 button = createToggleButton(ta); 455 } else { 456 button = createToggleButton(ta, getGroup(groupid, container)); 457 } 458 } 459 } 460 461 if (button == null) { 462 // Create a regular button 463 button = new JButton(action); 464 configureButtonFromExtActionProperties(button, action); 465 } 466 return button; 467 } 468 469 /** 470 * Creates a button based on the attributes of the action. 471 * 472 * @param action an action used to create the button 473 * @return will return a JButton or a JToggleButton. 474 */ 475 public AbstractButton createButton(Action action) { 476 return createButton(action, null); 477 } 478 479 /** 480 * Adds and configures a toggle button. 481 * @param a an abstraction of a toggle action. 482 */ 483 private JToggleButton createToggleButton(AbstractActionExt a) { 484 return createToggleButton(a, null); 485 } 486 487 /** 488 * Adds and configures a toggle button. 489 * @param a an abstraction of a toggle action. 490 * @param group the group to add the toggle button or null 491 */ 492 private JToggleButton createToggleButton(AbstractActionExt a, ButtonGroup group) { 493 JToggleButton button = new JToggleButton(); 494 configureButton(button, a, group); 495 return button; 496 } 497 498 /** 499 * 500 * @param button 501 * @param a 502 * @param group 503 */ 504 public void configureButton(JToggleButton button, AbstractActionExt a, ButtonGroup group) { 505 configureSelectableButton(button, a, group); 506 configureButtonFromExtActionProperties(button, a); 507 } 508 509 /** 510 * method to configure a "selectable" button from the given AbstractActionExt. 511 * As there is some un-/wiring involved to support synch of the selected property between 512 * the action and the button, all config and unconfig (== setting a null action!) 513 * should be passed through this method. <p> 514 * 515 * It's up to the client to only pass in button's where selected and/or the 516 * group property makes sense. 517 * 518 * PENDING: the group properties are yet untested. 519 * PENDING: think about automated unconfig. 520 * 521 * @param button where selected makes sense 522 * @param a 523 * @param group the button should be added to. 524 * @throws IllegalArgumentException if the given action doesn't have the state flag set. 525 * 526 */ 527 public void configureSelectableButton(AbstractButton button, AbstractActionExt a, ButtonGroup group){ 528 if ((a != null) && !a.isStateAction()) throw 529 new IllegalArgumentException("the Action must be a stateAction"); 530 // we assume that all button configuration is done exclusively through this method!! 531 if (button.getAction() == a) return; 532 533 // unconfigure if the old Action is a state AbstractActionExt 534 // PENDING JW: automate unconfigure via a PCL that is listening to 535 // the button's action property? Think about memory leak implications! 536 Action oldAction = button.getAction(); 537 if (oldAction instanceof AbstractActionExt) { 538 AbstractActionExt actionExt = (AbstractActionExt) oldAction; 539 // remove as itemListener 540 button.removeItemListener(actionExt); 541 // remove the button related PCL from the old actionExt 542 PropertyChangeListener[] l = actionExt.getPropertyChangeListeners(); 543 for (int i = l.length - 1; i >= 0; i--) { 544 if (l[i] instanceof ToggleActionPropertyChangeListener) { 545 ToggleActionPropertyChangeListener togglePCL = (ToggleActionPropertyChangeListener) l[i]; 546 if (togglePCL.isToggling(button)) { 547 actionExt.removePropertyChangeListener(togglePCL); 548 } 549 } 550 } 551 } 552 553 button.setAction(a); 554 if (group != null) { 555 group.add(button); 556 } 557 if (a != null) { 558 button.addItemListener(a); 559 // JW: move the initial config into the PCL?? 560 button.setSelected(a.isSelected()); 561 new ToggleActionPropertyChangeListener(a, button); 562 // new ToggleActionPCL(button, a); 563 } 564 565 } 566 567 /** 568 * This method will be called after buttons created from an action. Override 569 * for custom configuration. 570 * 571 * @param button the button to be configured 572 * @param action the action used to construct the menu item. 573 */ 574 protected void configureButtonFromExtActionProperties(AbstractButton button, Action action) { 575 if (action.getValue(Action.SHORT_DESCRIPTION) == null) { 576 button.setToolTipText((String)action.getValue(Action.NAME)); 577 } 578 // Use the large icon for toolbar buttons. 579 if (action.getValue(AbstractActionExt.LARGE_ICON) != null) { 580 button.setIcon((Icon)action.getValue(AbstractActionExt.LARGE_ICON)); 581 } 582 // Don't show the text under the toolbar buttons if they have an icon 583 if (button.getIcon() != null) { 584 button.setText(""); 585 } 586 } 587 588 589 /** 590 * This method will be called after menu items are created. 591 * Override for custom configuration. 592 * 593 * @param menuItem the menu item to be configured 594 * @param action the action used to construct the menu item. 595 */ 596 protected void configureMenuItemFromExtActionProperties(JMenuItem menuItem, Action action) { 597 } 598 599 /** 600 * Helper method to add a checkbox menu item. 601 */ 602 private JCheckBoxMenuItem createCheckBoxMenuItem(AbstractActionExt a) { 603 JCheckBoxMenuItem mi = new JCheckBoxMenuItem(); 604 configureSelectableButton(mi, a, null); 605 configureMenuItemFromExtActionProperties(mi, a); 606 return mi; 607 } 608 609 /** 610 * Helper method to add a radio button menu item. 611 */ 612 private JRadioButtonMenuItem createRadioButtonMenuItem(ButtonGroup group, 613 AbstractActionExt a) { 614 JRadioButtonMenuItem mi = new JRadioButtonMenuItem(); 615 configureSelectableButton(mi, a, group); 616 configureMenuItemFromExtActionProperties(mi, a); 617 return mi; 618 } 619 620 621 }