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