001/* 002 * $Id: ColumnControlButton.java 4065 2011-08-19 13:28:26Z 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 022package org.jdesktop.swingx.table; 023 024import java.awt.ComponentOrientation; 025import java.awt.Dimension; 026import java.awt.Insets; 027import java.awt.event.ActionEvent; 028import java.awt.event.ItemEvent; 029import java.beans.PropertyChangeEvent; 030import java.beans.PropertyChangeListener; 031import java.util.ArrayList; 032import java.util.Collections; 033import java.util.List; 034 035import javax.swing.AbstractAction; 036import javax.swing.Action; 037import javax.swing.Icon; 038import javax.swing.JButton; 039import javax.swing.JComboBox; 040import javax.swing.JComponent; 041import javax.swing.JMenuItem; 042import javax.swing.JPopupMenu; 043import javax.swing.SwingUtilities; 044import javax.swing.UIManager; 045import javax.swing.event.ChangeEvent; 046import javax.swing.event.ListSelectionEvent; 047import javax.swing.event.TableColumnModelEvent; 048import javax.swing.event.TableColumnModelListener; 049import javax.swing.plaf.UIResource; 050import javax.swing.table.TableColumn; 051import javax.swing.table.TableColumnModel; 052 053import org.jdesktop.swingx.JXTable; 054import org.jdesktop.swingx.action.AbstractActionExt; 055import org.jdesktop.swingx.action.ActionContainerFactory; 056import org.jdesktop.swingx.plaf.ColumnControlButtonAddon; 057import org.jdesktop.swingx.plaf.LookAndFeelAddons; 058import org.jdesktop.swingx.table.ColumnControlPopup.ActionGrouper; 059import org.jdesktop.swingx.table.ColumnControlPopup.ActionGroupable; 060 061/** 062 * A component to allow interactive customization of <code>JXTable</code>'s 063 * columns. 064 * It's main purpose is to allow toggling of table columns' visibility. 065 * Additionally, arbitrary configuration actions can be exposed. 066 * <p> 067 * 068 * This component is installed in the <code>JXTable</code>'s 069 * trailing corner, if enabled: 070 * 071 * <pre><code> 072 * table.setColumnControlVisible(true); 073 * </code></pre> 074 * 075 * From the perspective of a <code>JXTable</code>, the component's behaviour is 076 * opaque. Typically, the button's action is to popup a component for user 077 * interaction. <p> 078 * 079 * This class is responsible for handling/providing/updating the lists of 080 * actions and to keep each Action's state in synch with Table-/Column state. 081 * The visible behaviour of the popup is delegated to a 082 * <code>ColumnControlPopup</code>. <p> 083 * 084 * Default support for adding table (configuration or other) <code>Action</code>s is 085 * informal, driven by convention: 086 * <ul> 087 * <li> the JXTable's actionMap is scanned for candidate actions, the default marker 088 * is a key of type String which starts with {@link ColumnControlButton.COLUMN_CONTROL_MARKER} 089 * <li> the actions are sorted by that key and then handed over to the ColumnControlPopup 090 * for binding and addition of appropriate menu items 091 * <li> the addition as such is control by additionalActionsVisible property, its 092 * default value is true 093 * </ul> 094 * 095 * 096 * 097 * @see TableColumnExt 098 * @see TableColumnModelExt 099 * @see JXTable#setColumnControl 100 * 101 */ 102public class ColumnControlButton extends JButton { 103 104 // JW: really want to extend? for builders? 105 /** Marker to auto-recognize actions which should be added to the popup. */ 106 public static final String COLUMN_CONTROL_MARKER = "column."; 107 108 /** the key for looking up the control's icon in the UIManager. Typically, it's LAF dependent. */ 109 public static final String COLUMN_CONTROL_BUTTON_ICON_KEY = "ColumnControlButton.actionIcon"; 110 111 /** the key for looking up the control's margin in the UIManager. Typically, it's LAF dependent. */ 112 public static final String COLUMN_CONTROL_BUTTON_MARGIN_KEY = "ColumnControlButton.margin"; 113 114 static { 115 LookAndFeelAddons.contribute(new ColumnControlButtonAddon()); 116 } 117 118 /** exposed for testing. */ 119 protected ColumnControlPopup popup; 120 // TODO: the table reference is a potential leak? 121 /** The table which is controlled by this. */ 122 private JXTable table; 123 /** Listener for table property changes. */ 124 private PropertyChangeListener tablePropertyChangeListener; 125 /** Listener for table's columnModel. */ 126 TableColumnModelListener columnModelListener; 127 /** the list of actions for column menuitems.*/ 128 private List<ColumnVisibilityAction> columnVisibilityActions; 129 130 private boolean additionalActionsVisible; 131 132 133 /** 134 * Creates a column control button for the table. Uses the default 135 * icon as provided by the addon. 136 * 137 * @param table the <code>JXTable</code> controlled by this component 138 */ 139 public ColumnControlButton(JXTable table) { 140 this(table, null); 141 } 142 143 /** 144 * Creates a column control button for the table. The button 145 * uses the given icon and has no text. 146 * @param table the <code>JXTable</code> controlled by this component 147 * @param icon the <code>Icon</code> to show 148 */ 149 public ColumnControlButton(JXTable table, Icon icon) { 150 super(); 151 init(); 152 // JW: icon LF dependent? 153 setAction(createControlAction(icon)); 154 updateActionUI(); 155 updateButtonUI(); 156 installTable(table); 157 } 158 159 160 @Override 161 public void updateUI() { 162 super.updateUI(); 163 // JW: icon may be LF dependent 164 updateActionUI(); 165 updateButtonUI(); 166 getColumnControlPopup().updateUI(); 167 } 168 169 /** 170 * Updates this button's properties provided by the LAF. 171 * Here: overwrites the action's small_icon with the icon from the ui if the current 172 * icon is null or a UIResource. 173 */ 174 protected void updateButtonUI() { 175 if ((getMargin() == null) || (getMargin() instanceof UIResource)) { 176 Insets insets = UIManager.getInsets(COLUMN_CONTROL_BUTTON_MARGIN_KEY); 177 setMargin(insets); 178 } 179 } 180 181 /** 182 * Updates the action properties provided by the LAF. 183 * Here: overwrites the action's small_icon with the icon from the ui if the current 184 * icon is null or a UIResource. 185 */ 186 protected void updateActionUI() { 187 if (getAction() == null) return; 188 Icon icon = (Icon) getAction().getValue(Action.SMALL_ICON); 189 if ((icon == null) || (icon instanceof UIResource)) { 190 icon = UIManager.getIcon(COLUMN_CONTROL_BUTTON_ICON_KEY); 191 getAction().putValue(Action.SMALL_ICON, icon); 192 } 193 } 194 195 /** 196 * Toggles the popup component's visibility. This method is 197 * called by this control's default action. <p> 198 * 199 * Here: delegates to getControlPopup(). 200 */ 201 public void togglePopup() { 202 getColumnControlPopup().toggleVisibility(this); 203 } 204 205 /** 206 * Returns the actionsVisible property which controls whether or not 207 * additional table Actions should be included into the popup. 208 * 209 * @return a boolean indicating whether or not additional table Actions 210 * are visible 211 */ 212 public boolean getAdditionalActionsVisible() { 213 return additionalActionsVisible; 214 } 215 216 217 /** 218 * Sets the additonalActionsVisible property. It controls whether or 219 * not additional table actions should be included into the popup. <p> 220 * 221 * The default value is <code>true</code>. 222 * 223 * @param additionalActionsVisible the additionalActionsVisible to set 224 */ 225 public void setAdditionalActionsVisible(boolean additionalActionsVisible) { 226 if (additionalActionsVisible == getAdditionalActionsVisible()) return; 227 boolean old = getAdditionalActionsVisible(); 228 this.additionalActionsVisible = additionalActionsVisible; 229 populatePopup(); 230 firePropertyChange("additionalActionsVisible", old, getAdditionalActionsVisible()); 231 } 232 233 /** 234 * Sets the grouper to use for grouping the additional actions. Maybe null to 235 * have no additional grouping. Has no effect 236 * if the ColumnControlPopup doesn't implement Groupable. The default 237 * ColumnControlPopup supports Groupable, but is instantiated without a Grouper. 238 * 239 * @param grouper 240 */ 241 public void setActionGrouper(ActionGrouper grouper) { 242 if (!(getColumnControlPopup() instanceof ActionGroupable)) return; 243 ((ActionGroupable) getColumnControlPopup()).setActionGrouper(grouper); 244 populatePopup(); 245 } 246 247 @Override 248 public void applyComponentOrientation(ComponentOrientation o) { 249 super.applyComponentOrientation(o); 250 getColumnControlPopup().applyComponentOrientation(o); 251 } 252 253 254//-------------------------- Action in synch with column properties 255 /** 256 * A specialized <code>Action</code> which takes care of keeping in synch with 257 * TableColumn state. 258 * 259 * NOTE: client must call releaseColumn if this action is no longer needed! 260 * 261 */ 262 public class ColumnVisibilityAction extends AbstractActionExt { 263 264 private TableColumn column; 265 266 private PropertyChangeListener columnListener; 267 268 /** flag to distinguish selection changes triggered by 269 * column's property change from those triggered by 270 * user interaction. Hack around #212-swingx. 271 */ 272 private boolean fromColumn; 273 274 /** 275 * Creates a action synched to the table column. 276 * 277 * @param column the <code>TableColumn</code> to keep synched to. 278 */ 279 public ColumnVisibilityAction(TableColumn column) { 280 super((String) null); 281 setStateAction(); 282 installColumn(column); 283 } 284 285 /** 286 * Releases all references to the synched <code>TableColumn</code>. 287 * Client code must call this method if the 288 * action is no longer needed. After calling this action must not be 289 * used any longer. 290 */ 291 public void releaseColumn() { 292 column.removePropertyChangeListener(columnListener); 293 column = null; 294 } 295 296 /** 297 * Returns true if the action is enabled. Returns 298 * true only if the action is enabled and the table 299 * column can be controlled. 300 * 301 * @return true if the action is enabled, false otherwise 302 * @see #canControlColumn() 303 */ 304 @Override 305 public boolean isEnabled() { 306 return super.isEnabled() && canControlColumn(); 307 } 308 309 /** 310 * Returns flag to indicate if column's visibility can 311 * be controlled. Minimal requirement is that column is of type 312 * <code>TableColumnExt</code>. 313 * 314 * @return boolean to indicate if columns's visibility can be controlled. 315 */ 316 protected boolean canControlColumn() { 317 // JW: should have direction? control is from action to column, the 318 // other way round should be guaranteed always 319 return (column instanceof TableColumnExt); 320 } 321 322 @Override 323 public void itemStateChanged(final ItemEvent e) { 324 if (canControlColumn()) { 325 if ((e.getStateChange() == ItemEvent.DESELECTED) 326 //JW: guarding against 1 leads to #212-swingx: setting 327 // column visibility programatically fails if 328 // the current column is the second last visible 329 // guarding against 0 leads to hiding all columns 330 // by deselecting the menu item. 331 && (table.getColumnCount() <= 1) 332 // JW Fixed #212: basically implemented Rob's idea to distinguish 333 // event sources instead of unconditionally reselect 334 // not entirely sure if the state transitions are completely 335 // defined but all related tests are passing now. 336 && !fromColumn) { 337 reselect(); 338 } else { 339 setSelected(e.getStateChange() == ItemEvent.SELECTED); 340 } 341 } 342 } 343 344 345 @Override 346 public synchronized void setSelected(boolean newValue) { 347 super.setSelected(newValue); 348 if (canControlColumn()) { 349 if (!fromColumn) 350 ((TableColumnExt) column).setVisible(newValue); 351 } 352 } 353 354 /** 355 * Does nothing. Synch from action state to TableColumn state 356 * is done in itemStateChanged. 357 */ 358 @Override 359 public void actionPerformed(ActionEvent e) { 360 361 } 362 363 /** 364 * Synchs selected property to visible. This 365 * is called on change of tablecolumn's <code>visible</code> property. 366 * 367 * @param visible column visible state to synch to. 368 */ 369 private void updateFromColumnVisible(boolean visible) { 370// /*boolean*/ visible = true; 371// if (canControlColumn()) { 372// visible = ((TableColumnExt) column).isVisible(); 373// } 374 fromColumn = true; 375 setSelected(visible); 376 fromColumn = false; 377 } 378 379 380 protected void updateFromColumnHideable(boolean hideable) { 381 setEnabled(hideable); 382 } 383 384 /** 385 * Synchs name property to value. This is called on change of 386 * tableColumn's <code>headerValue</code> property. 387 * 388 * @param value 389 */ 390 private void updateFromColumnHeader(Object value) { 391 setName(String.valueOf(value)); 392 } 393 394 /** 395 * Enforces selected to <code>true</code>. Called if user interaction 396 * tried to de-select the last single visible column. 397 * 398 */ 399 private void reselect() { 400 firePropertyChange("selected", null, Boolean.TRUE); 401 } 402 403 // -------------- init 404 private void installColumn(TableColumn column) { 405 this.column = column; 406 column.addPropertyChangeListener(getColumnListener()); 407 updateFromColumnHeader(column.getHeaderValue()); 408 // #429-swing: actionCommand must be string 409 if (column.getIdentifier() != null) { 410 setActionCommand(column.getIdentifier().toString()); 411 } 412 boolean visible = (column instanceof TableColumnExt) ? 413 ((TableColumnExt) column).isVisible() : true; 414 updateFromColumnVisible(visible); 415 } 416 417 /** 418 * Returns the listener to column's property changes. The listener 419 * is created lazily if necessary. 420 * 421 * @return the <code>PropertyChangeListener</code> listening to 422 * <code>TableColumn</code>'s property changes, guaranteed to be 423 * not <code>null</code>. 424 */ 425 protected PropertyChangeListener getColumnListener() { 426 if (columnListener == null) { 427 columnListener = createPropertyChangeListener(); 428 } 429 return columnListener; 430 } 431 432 /** 433 * Creates and returns the listener to column's property changes. 434 * Subclasses are free to roll their own. 435 * <p> 436 * Implementation note: this listener reacts to column's 437 * <code>visible</code> and <code>headerValue</code> properties and 438 * calls the respective <code>updateFromXX</code> methodes. 439 * 440 * @return the <code>PropertyChangeListener</code> to use with the 441 * column 442 */ 443 protected PropertyChangeListener createPropertyChangeListener() { 444 return new PropertyChangeListener() { 445 @Override 446 public void propertyChange(PropertyChangeEvent evt) { 447 if ("visible".equals(evt.getPropertyName())) { 448 updateFromColumnVisible((Boolean) evt.getNewValue()); 449 } else if ("headerValue".equals(evt.getPropertyName())) { 450 updateFromColumnHeader(evt.getNewValue()); 451 } else if ("hideable".equals(evt.getPropertyName())) { 452 updateFromColumnHideable((Boolean) evt.getNewValue()); 453 } 454 } 455 }; 456 } 457 } 458 459 // ---------------------- the popup 460 461 /** 462 * A default implementation of ColumnControlPopup. 463 * It uses a JPopupMenu with 464 * MenuItems corresponding to the Actions as 465 * provided by the ColumnControlButton. 466 * 467 * 468 */ 469 public class DefaultColumnControlPopup implements ColumnControlPopup, ActionGroupable { 470 private JPopupMenu popupMenu; 471 private ActionGrouper grouper; 472 473 public DefaultColumnControlPopup() { 474 this(null); 475 } 476 477 //------------------ public methods to control visibility status 478 479 public DefaultColumnControlPopup(ActionGrouper grouper) { 480 this.grouper = grouper; 481 } 482 483 /** 484 * @inheritDoc 485 * 486 */ 487 @Override 488 public void updateUI() { 489 SwingUtilities.updateComponentTreeUI(getPopupMenu()); 490 } 491 492 /** 493 * @inheritDoc 494 * 495 */ 496 @Override 497 public void toggleVisibility(JComponent owner) { 498 JPopupMenu popupMenu = getPopupMenu(); 499 if (popupMenu.isVisible()) { 500 popupMenu.setVisible(false); 501 } else if (popupMenu.getComponentCount() > 0) { 502 Dimension buttonSize = owner.getSize(); 503 int xPos = owner.getComponentOrientation().isLeftToRight() ? buttonSize.width 504 - popupMenu.getPreferredSize().width 505 : 0; 506 popupMenu.show(owner, 507 // JW: trying to allow popup without CCB showing 508 // weird behaviour 509// owner.isShowing()? owner : null, 510 xPos, buttonSize.height); 511 } 512 513 } 514 515 /** 516 * @inheritDoc 517 * 518 */ 519 @Override 520 public void applyComponentOrientation(ComponentOrientation o) { 521 getPopupMenu().applyComponentOrientation(o); 522 523 } 524 525 //-------------------- public methods to manipulate popup contents. 526 527 /** 528 * @inheritDoc 529 * 530 */ 531 @Override 532 public void removeAll() { 533 getPopupMenu().removeAll(); 534 } 535 536 537 /** 538 * @inheritDoc 539 * 540 */ 541 @Override 542 public void addVisibilityActionItems( 543 List<? extends AbstractActionExt> actions) { 544 addItems(new ArrayList<Action>(actions)); 545 546 } 547 548 549 /** 550 * @inheritDoc 551 * 552 */ 553 @Override 554 public void addAdditionalActionItems(List<? extends Action> actions) { 555 if (actions.size() == 0) 556 return; 557 // JW: this is a reference to the enclosing class 558 // prevents to make this implementation static 559 // Hmmm...any way around? 560 if (canControl()) { 561 addSeparator(); 562 } 563 564 if (getGrouper() == null) { 565 addItems(actions); 566 return; 567 } 568 List<? extends List<? extends Action>> groups = grouper.group(actions); 569 for (List<? extends Action> group : groups) { 570 addItems(group); 571 if (group != groups.get(groups.size()- 1)) 572 addSeparator(); 573 574 } 575 576 } 577 578 //--------------------------- internal helpers to manipulate popups content 579 580 /** 581 * Here: creates and adds a menuItem to the popup for every 582 * Action in the list. Does nothing if 583 * if the list is empty. 584 * 585 * PRE: actions != null. 586 * 587 * @param actions a list containing the actions to add to the popup. 588 * Must not be null. 589 * 590 */ 591 protected void addItems(List<? extends Action> actions) { 592 ActionContainerFactory factory = new ActionContainerFactory(null); 593 for (Action action : actions) { 594 addItem(factory.createMenuItem(action)); 595 } 596 } 597 598 /** 599 * adds a separator to the popup. 600 * 601 */ 602 protected void addSeparator() { 603 getPopupMenu().addSeparator(); 604 } 605 606 /** 607 * 608 * @param item the menuItem to add to the popup. 609 */ 610 protected void addItem(JMenuItem item) { 611 getPopupMenu().add(item); 612 } 613 614 /** 615 * 616 * @return the popup to add menuitems. Guaranteed to be != null. 617 */ 618 protected JPopupMenu getPopupMenu() { 619 if (popupMenu == null) { 620 popupMenu = new JPopupMenu(); 621 } 622 return popupMenu; 623 } 624 625 // --------------- implement Groupable 626 627 @Override 628 public void setActionGrouper(ActionGrouper grouper) { 629 this.grouper = grouper; 630 } 631 632 protected ActionGrouper getGrouper() { 633 return grouper; 634 } 635 636 } 637 /** 638 * Returns to popup component for user interaction. Lazily 639 * creates the component if necessary. 640 * 641 * @return the ColumnControlPopup for showing the items, guaranteed 642 * to be not <code>null</code>. 643 * @see #createColumnControlPopup() 644 */ 645 protected ColumnControlPopup getColumnControlPopup() { 646 if (popup == null) { 647 popup = createColumnControlPopup(); 648 } 649 return popup; 650 } 651 652 /** 653 * Factory method to return a <code>ColumnControlPopup</code>. 654 * Subclasses can override to hook custom implementations. 655 * 656 * @return the <code>ColumnControlPopup</code> used. 657 */ 658 protected ColumnControlPopup createColumnControlPopup() { 659 return new DefaultColumnControlPopup(); 660 } 661 662 663//-------------------------- updates from table propertyChangelistnere 664 665 /** 666 * Adjusts internal state after table's column model property has changed. 667 * Handles cleanup of listeners to the old/new columnModel (Note, that 668 * it listens to the column model only if it can control column visibility). 669 * Updates content of popup. 670 * 671 * @param oldModel the old <code>TableColumnModel</code> we had been listening to. 672 */ 673 protected void updateFromColumnModelChange(TableColumnModel oldModel) { 674 if (oldModel != null) { 675 oldModel.removeColumnModelListener(columnModelListener); 676 } 677 populatePopup(); 678 if (canControl()) { 679 table.getColumnModel().addColumnModelListener(getColumnModelListener()); 680 } 681 } 682 683 /** 684 * Synchs this button's enabled with table's enabled. 685 * 686 */ 687 protected void updateFromTableEnabledChanged() { 688 getAction().setEnabled(table.isEnabled()); 689 690 } 691 /** 692 * Method to check if we can control column visibility POST: if true we can 693 * be sure to have an extended TableColumnModel 694 * 695 * @return boolean to indicate if controlling the visibility state is 696 * possible. 697 */ 698 protected boolean canControl() { 699 return table.getColumnModel() instanceof TableColumnModelExt; 700 } 701 702// ------------------------ updating the popup 703 /** 704 * Populates the popup from scratch. 705 * 706 * If applicable, creates and adds column visibility actions. Always adds 707 * additional actions. 708 */ 709 protected void populatePopup() { 710 clearAll(); 711 if (canControl()) { 712 createVisibilityActions(); 713 addVisibilityActionItems(); 714 } 715 addAdditionalActionItems(); 716 } 717 718 /** 719 * 720 * removes all components from the popup, making sure to release all 721 * columnVisibility actions. 722 * 723 */ 724 protected void clearAll() { 725 clearColumnVisibilityActions(); 726 getColumnControlPopup().removeAll(); 727 } 728 729 730 /** 731 * Releases actions and clears list of actions. 732 * 733 */ 734 protected void clearColumnVisibilityActions() { 735 if (columnVisibilityActions == null) 736 return; 737 for (ColumnVisibilityAction action : columnVisibilityActions) { 738 action.releaseColumn(); 739 } 740 columnVisibilityActions.clear(); 741 } 742 743 744 /** 745 * Adds visibility actions into the popup view. 746 * 747 * Here: delegates the list of actions to the DefaultColumnControlPopup. 748 * <p> 749 * PRE: columnVisibilityActions populated before calling this. 750 * 751 */ 752 protected void addVisibilityActionItems() { 753 getColumnControlPopup().addVisibilityActionItems( 754 Collections.unmodifiableList(getColumnVisibilityActions())); 755 } 756 757 /** 758 * Adds additional actions to the popup, if additionalActionsVisible is true, 759 * does nothing otherwise.<p> 760 * 761 * Here: delegates the list of actions as returned by #getAdditionalActions() 762 * to the DefaultColumnControlPopup. 763 * Does nothing if #getColumnActions() is empty. 764 * 765 */ 766 protected void addAdditionalActionItems() { 767 if (!getAdditionalActionsVisible()) return; 768 getColumnControlPopup().addAdditionalActionItems( 769 Collections.unmodifiableList(getAdditionalActions())); 770 } 771 772 773 /** 774 * Creates and adds a ColumnVisiblityAction for every column that should be 775 * togglable via the column control. <p> 776 * 777 * Here: all table columns contained in the <code>TableColumnModel</code> - 778 * visible and invisible columns - to <code>createColumnVisibilityAction</code> and 779 * adds all not <code>null</code> return values. 780 * 781 * <p> 782 * PRE: canControl() 783 * 784 * @see #createColumnVisibilityAction 785 */ 786 protected void createVisibilityActions() { 787 List<TableColumn> columns = table.getColumns(true); 788 for (TableColumn column : columns) { 789 ColumnVisibilityAction action = createColumnVisibilityAction(column); 790 if (action != null) { 791 getColumnVisibilityActions().add(action); 792 } 793 } 794 795 } 796 797 /** 798 * Creates and returns a <code>ColumnVisibilityAction</code> for the given 799 * <code>TableColumn</code>. The return value might be null, f.i. if the 800 * column should not be allowed to be toggled. 801 * 802 * @param column the <code>TableColumn</code> to use for the action 803 * @return a ColumnVisibilityAction to use for the given column, 804 * may be <code>null</code>. 805 */ 806 protected ColumnVisibilityAction createColumnVisibilityAction(TableColumn column) { 807 return new ColumnVisibilityAction(column); 808 } 809 810 /** 811 * Lazyly creates and returns the List of visibility actions. 812 * 813 * @return the list of visibility actions, guaranteed to be != null. 814 */ 815 protected List<ColumnVisibilityAction> getColumnVisibilityActions() { 816 if (columnVisibilityActions == null) { 817 columnVisibilityActions = new ArrayList<ColumnVisibilityAction>(); 818 } 819 return columnVisibilityActions; 820 } 821 822 823 /** 824 * creates and returns a list of additional Actions to add to the popup. 825 * Here: the actions are looked up in the table's actionMap according 826 * to the keys as returned from #getColumnControlActionKeys(); 827 * 828 * @return a list containing all additional actions to include into the popup. 829 */ 830 protected List<Action> getAdditionalActions() { 831 List<?> actionKeys = getColumnControlActionKeys(); 832 List<Action> actions = new ArrayList<Action>(); 833 for (Object key : actionKeys) { 834 actions.add(table.getActionMap().get(key)); 835 } 836 return actions; 837 } 838 839 /** 840 * Looks up and returns action keys to access actions in the 841 * table's actionMap which should be included into the popup. 842 * 843 * Here: all keys with isColumnControlActionKey(key). The list 844 * is sorted by those keys. 845 * 846 * @return the action keys of table's actionMap entries whose 847 * action should be included into the popup. 848 */ 849 @SuppressWarnings({ "unchecked", "rawtypes" }) 850 protected List getColumnControlActionKeys() { 851 Object[] allKeys = table.getActionMap().allKeys(); 852 List columnKeys = new ArrayList(); 853 for (int i = 0; i < allKeys.length; i++) { 854 if (isColumnControlActionKey(allKeys[i])) { 855 columnKeys.add(allKeys[i]); 856 } 857 } 858 // JW: this will blow for non-String keys! 859 // so this method is less decoupled from the 860 // decision method isControl than expected. 861 Collections.sort(columnKeys); 862 return columnKeys; 863 } 864 865 /** 866 * Here: true if a String key starts with #COLUMN_CONTROL_MARKER. 867 * 868 * @param actionKey a key in the table's actionMap. 869 * @return a boolean to indicate whether the given actionKey maps to 870 * an action which should be included into the popup. 871 * 872 */ 873 protected boolean isColumnControlActionKey(Object actionKey) { 874 return (actionKey instanceof String) && 875 ((String) actionKey).startsWith(COLUMN_CONTROL_MARKER); 876 } 877 878 879 //--------------------------- init 880 881 private void installTable(JXTable table) { 882 this.table = table; 883 table.addPropertyChangeListener(getTablePropertyChangeListener()); 884 updateFromColumnModelChange(null); 885 updateFromTableEnabledChanged(); 886 } 887 888 889 /** 890 * Initialize the column control button's gui 891 */ 892 private void init() { 893 setFocusPainted(false); 894 setFocusable(false); 895 // this is a trick to get hold of the client prop which 896 // prevents closing of the popup 897 JComboBox box = new JComboBox(); 898 Object preventHide = box.getClientProperty("doNotCancelPopup"); 899 putClientProperty("doNotCancelPopup", preventHide); 900 additionalActionsVisible = true; 901 } 902 903 904 /** 905 * Creates and returns the default action for this button. 906 * @param icon 907 * 908 * @param icon the Icon to use in the action. 909 * @return the default action. 910 */ 911 private Action createControlAction(Icon icon) { 912 913 Action control = new AbstractAction() { 914 915 @Override 916 public void actionPerformed(ActionEvent e) { 917 togglePopup(); 918 } 919 920 }; 921 control.putValue(Action.SMALL_ICON, icon); 922 return control; 923 } 924 925 // -------------------------------- listeners 926 927 /** 928 * Returns the listener to table's property changes. The listener is 929 * lazily created if necessary. 930 * @return the <code>PropertyChangeListener</code> for use with the 931 * table, guaranteed to be not <code>null</code>. 932 */ 933 protected PropertyChangeListener getTablePropertyChangeListener() { 934 if (tablePropertyChangeListener == null) { 935 tablePropertyChangeListener = createTablePropertyChangeListener(); 936 } 937 return tablePropertyChangeListener; 938 } 939 940 /** 941 * Creates the listener to table's property changes. Subclasses are free 942 * to roll their own. <p> 943 * Implementation note: this listener reacts to table's <code>enabled</code> and 944 * <code>columnModel</code> properties and calls the respective 945 * <code>updateFromXX</code> methodes. 946 * 947 * @return the <code>PropertyChangeListener</code> for use with the table. 948 */ 949 protected PropertyChangeListener createTablePropertyChangeListener() { 950 return new PropertyChangeListener() { 951 @Override 952 public void propertyChange(PropertyChangeEvent evt) { 953 if ("columnModel".equals(evt.getPropertyName())) { 954 updateFromColumnModelChange((TableColumnModel) evt 955 .getOldValue()); 956 } else if ("enabled".equals(evt.getPropertyName())) { 957 updateFromTableEnabledChanged(); 958 } 959 } 960 }; 961 } 962 963 /** 964 * Returns the listener to table's column model. The listener is 965 * lazily created if necessary. 966 * @return the <code>TableColumnModelListener</code> for use with the 967 * table's column model, guaranteed to be not <code>null</code>. 968 */ 969 protected TableColumnModelListener getColumnModelListener() { 970 if (columnModelListener == null) { 971 columnModelListener = createColumnModelListener(); 972 } 973 return columnModelListener; 974 } 975 976 /** 977 * Creates the listener to columnModel. Subclasses are free to roll their 978 * own. 979 * <p> 980 * Implementation note: this listener reacts to "real" columnRemoved/-Added by 981 * populating the popups content from scratch. 982 * 983 * @return the <code>TableColumnModelListener</code> for use with the 984 * table's columnModel. 985 */ 986 protected TableColumnModelListener createColumnModelListener() { 987 return new TableColumnModelListener() { 988 /** Tells listeners that a column was added to the model. */ 989 @Override 990 public void columnAdded(TableColumnModelEvent e) { 991 // quickfix for #192 992 if (!isVisibilityChange(e, true)) { 993 populatePopup(); 994 } 995 } 996 997 /** Tells listeners that a column was removed from the model. */ 998 @Override 999 public void columnRemoved(TableColumnModelEvent e) { 1000 if (!isVisibilityChange(e, false)) { 1001 populatePopup(); 1002 } 1003 } 1004 1005 /** 1006 * Check if the add/remove event is triggered by a move to/from the 1007 * invisible columns. 1008 * 1009 * PRE: the event must be received in columnAdded/Removed. 1010 * 1011 * @param e the received event 1012 * @param added if true the event is assumed to be received via 1013 * columnAdded, otherwise via columnRemoved. 1014 * @return boolean indicating whether the removed/added is a side-effect 1015 * of hiding/showing the column. 1016 */ 1017 private boolean isVisibilityChange(TableColumnModelEvent e, 1018 boolean added) { 1019 // can't tell 1020 if (!(e.getSource() instanceof DefaultTableColumnModelExt)) 1021 return false; 1022 DefaultTableColumnModelExt model = (DefaultTableColumnModelExt) e 1023 .getSource(); 1024 if (added) { 1025 return model.isAddedFromInvisibleEvent(e.getToIndex()); 1026 } else { 1027 return model.isRemovedToInvisibleEvent(e.getFromIndex()); 1028 } 1029 } 1030 1031 /** Tells listeners that a column was repositioned. */ 1032 @Override 1033 public void columnMoved(TableColumnModelEvent e) { 1034 } 1035 1036 /** Tells listeners that a column was moved due to a margin change. */ 1037 @Override 1038 public void columnMarginChanged(ChangeEvent e) { 1039 } 1040 1041 /** 1042 * Tells listeners that the selection model of the TableColumnModel 1043 * changed. 1044 */ 1045 @Override 1046 public void columnSelectionChanged(ListSelectionEvent e) { 1047 } 1048 }; 1049 } 1050 1051 1052} // end class ColumnControlButton