001 /* 002 * $Id: ColumnControlButton.java,v 1.18 2005/12/02 12:54:03 kleopatra 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 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.Arrays; 033 import java.util.Iterator; 034 import java.util.List; 035 036 import javax.swing.AbstractAction; 037 import javax.swing.Action; 038 import javax.swing.Icon; 039 import javax.swing.JButton; 040 import javax.swing.JComboBox; 041 import javax.swing.JPopupMenu; 042 import javax.swing.event.ChangeEvent; 043 import javax.swing.event.ListSelectionEvent; 044 import javax.swing.event.TableColumnModelEvent; 045 import javax.swing.event.TableColumnModelListener; 046 import javax.swing.table.TableColumn; 047 import javax.swing.table.TableColumnModel; 048 049 import org.jdesktop.swingx.JXTable; 050 import org.jdesktop.swingx.action.AbstractActionExt; 051 import org.jdesktop.swingx.action.ActionContainerFactory; 052 053 /** 054 * This class is installed in the upper right corner of the table and is a 055 * control which allows for toggling the visibilty of individual columns. 056 * 057 * TODO: the table reference is a potential leak 058 * 059 * TODO: no need to extend JButton - use non-visual controller returning 060 * a JComponent instead. 061 * 062 */ 063 public final class ColumnControlButton extends JButton { 064 065 /** exposed for testing. */ 066 protected JPopupMenu popupMenu = null; 067 /** the table which is controlled by this. */ 068 private JXTable table; 069 /** a marker to auto-recognize actions which should be added to the popup */ 070 public static final String COLUMN_CONTROL_MARKER = "column."; 071 /** the list of actions for column menuitems.*/ 072 private List<ColumnVisibilityAction> columnVisibilityActions; 073 074 public ColumnControlButton(JXTable table, Icon icon) { 075 super(); 076 init(); 077 setAction(createControlAction(icon)); 078 installTable(table); 079 } 080 081 public void updateUI() { 082 super.updateUI(); 083 setMargin(new Insets(1, 2, 2, 1)); // Make this LAF-independent 084 if (popupMenu != null) { 085 // JW: Hmm, not really working.... 086 popupMenu.updateUI(); 087 } 088 } 089 090 091 //-------------------------- Action in synch with column properties 092 /** 093 * A specialized action which takes care of keeping in synch with 094 * TableColumn state. 095 * 096 * NOTE: client must call releaseColumn if this action is no longer needed! 097 * 098 */ 099 public class ColumnVisibilityAction extends AbstractActionExt { 100 101 private TableColumn column; 102 103 private PropertyChangeListener columnListener; 104 105 public ColumnVisibilityAction(TableColumn column) { 106 super((String) null); 107 setStateAction(); 108 installColumn(column); 109 } 110 111 /** 112 * 113 * release listening to column. Client must call this method if the 114 * action is no longer needed. After calling it the action must not be 115 * used any longer. 116 */ 117 public void releaseColumn() { 118 column.removePropertyChangeListener(columnListener); 119 column = null; 120 } 121 122 public boolean isEnabled() { 123 return super.isEnabled() && canControl(); 124 } 125 126 private boolean canControl() { 127 return (column instanceof TableColumnExt); 128 } 129 130 public void itemStateChanged(final ItemEvent e) { 131 if (canControl()) { 132 if ((e.getStateChange() == ItemEvent.DESELECTED) 133 //JW: guarding against 1 leads to #212-swingx: setting 134 // column visibility programatically fails if 135 // the current column is the second last visible 136 // guarding against 0 leads to hiding all columns 137 // by deselecting the menu item. 138 // TODO further check Rob's basic idea to distinguish 139 // event sources instead of unconditionally reselect! 140 && (table.getColumnCount() <= 1)) { 141 reselect(); 142 } else { 143 ((TableColumnExt) column) 144 .setVisible(e.getStateChange() == ItemEvent.SELECTED); 145 } 146 } 147 } 148 149 public void actionPerformed(ActionEvent e) { 150 // TODO Auto-generated method stub 151 152 } 153 154 private void updateSelected() { 155 boolean visible = true; 156 if (canControl()) { 157 visible = ((TableColumnExt) column).isVisible(); 158 } 159 setSelected(visible); 160 } 161 162 private void reselect() { 163 firePropertyChange("selected", null, Boolean.TRUE); 164 } 165 166 // -------------- init 167 private void installColumn(TableColumn column) { 168 this.column = column; 169 column.addPropertyChangeListener(getColumnListener()); 170 setName(String.valueOf(column.getHeaderValue())); 171 setActionCommand(column.getIdentifier()); 172 updateSelected(); 173 } 174 175 private PropertyChangeListener getColumnListener() { 176 if (columnListener == null) { 177 columnListener = createPropertyChangeListener(); 178 } 179 return columnListener; 180 } 181 182 private PropertyChangeListener createPropertyChangeListener() { 183 PropertyChangeListener l = new PropertyChangeListener() { 184 185 public void propertyChange(PropertyChangeEvent evt) { 186 if ("visible".equals(evt.getPropertyName())) { 187 updateSelected(); 188 } else if ("headerValue".equals(evt.getPropertyName())) { 189 setName(String.valueOf(evt.getNewValue())); 190 } 191 } 192 193 }; 194 return l; 195 } 196 197 } 198 199 /** 200 * open the popup. 201 * 202 * hmmm... really public? 203 * 204 * 205 */ 206 public void togglePopup() { 207 if (popupMenu.isVisible()) { 208 popupMenu.setVisible(false); 209 } else if (popupMenu.getComponentCount() > 0) { 210 Dimension buttonSize = getSize(); 211 int xPos = getComponentOrientation().isLeftToRight() ? 212 buttonSize.width - popupMenu.getPreferredSize().width : 0; 213 popupMenu.show(this, xPos, buttonSize.height); 214 } 215 } 216 217 @Override 218 public void applyComponentOrientation(ComponentOrientation o) { 219 super.applyComponentOrientation(o); 220 popupMenu.applyComponentOrientation(o); 221 } 222 223 //-------------------------- updates from table propertyChangelistnere 224 225 /** 226 * adjust internal state to after table's column model property has changed. 227 * Handles cleanup of listeners to the old/new columnModel (listens to the 228 * new only if we can control column visibility) and content of popupMenu. 229 * 230 * @param oldModel 231 */ 232 private void updateFromColumnModelChange(TableColumnModel oldModel) { 233 if (oldModel != null) { 234 oldModel.removeColumnModelListener(columnModelListener); 235 } 236 populatePopupMenu(); 237 if (canControl()) { 238 table.getColumnModel().addColumnModelListener(columnModelListener); 239 } 240 } 241 242 protected void updateFromTableEnabledChanged() { 243 getAction().setEnabled(table.isEnabled()); 244 245 } 246 /** 247 * Method to check if we can control column visibility POST: if true we can 248 * be sure to have an extended TableColumnModel 249 * 250 * @return 251 */ 252 private boolean canControl() { 253 return table.getColumnModel() instanceof TableColumnModelExt; 254 } 255 256 // ------------------------ updating the popupMenu 257 /** 258 * Populates the popup menu with actions related to controlling columns. 259 * Adds an action for each columns in the table (including both visible and 260 * hidden). Adds all column-related actions from the table's actionMap. 261 */ 262 protected void populatePopupMenu() { 263 clearPopupMenu(); 264 if (canControl()) { 265 addColumnMenuItems(); 266 } 267 addColumnActions(); 268 } 269 270 /** 271 * 272 * removes all components from the popup, making sure to release all 273 * columnVisibility actions. 274 * 275 */ 276 private void clearPopupMenu() { 277 clearColumnVisibilityActions(); 278 popupMenu.removeAll(); 279 } 280 281 /** 282 * release actions and clear list of actions. 283 * 284 */ 285 private void clearColumnVisibilityActions() { 286 if (columnVisibilityActions == null) 287 return; 288 for (Iterator<ColumnVisibilityAction> iter = columnVisibilityActions 289 .iterator(); iter.hasNext();) { 290 iter.next().releaseColumn(); 291 292 } 293 columnVisibilityActions.clear(); 294 } 295 296 297 /** 298 * adds a action to toggle column visibility for every column currently in 299 * the table. This includes both visible and invisible columns. 300 * 301 * pre: canControl() 302 * 303 */ 304 private void addColumnMenuItems() { 305 // For each column in the view, add a JCheckBoxMenuItem to popup 306 List columns = table.getColumns(true); 307 ActionContainerFactory factory = new ActionContainerFactory(null); 308 for (Iterator iter = columns.iterator(); iter.hasNext();) { 309 TableColumn column = (TableColumn) iter.next(); 310 ColumnVisibilityAction action = new ColumnVisibilityAction(column); 311 getColumnVisibilityActions().add(action); 312 popupMenu.add(factory.createMenuItem(action)); 313 } 314 315 } 316 317 private List<ColumnVisibilityAction> getColumnVisibilityActions() { 318 if (columnVisibilityActions == null) { 319 columnVisibilityActions = new ArrayList<ColumnVisibilityAction>(); 320 } 321 return columnVisibilityActions; 322 } 323 324 /** 325 * add additional actions from the xtable's actionMap. 326 * 327 */ 328 private void addColumnActions() { 329 Object[] actionKeys = getColumnControlActionKeys(); 330 Arrays.sort(actionKeys); 331 List actions = new ArrayList(); 332 for (int i = 0; i < actionKeys.length; i++) { 333 if (isColumnControlActionKey(actionKeys[i])) { 334 actions.add(table.getActionMap().get(actionKeys[i])); 335 } 336 } 337 if (actions.size() == 0) 338 return; 339 if (canControl()) { 340 popupMenu.addSeparator(); 341 } 342 ActionContainerFactory factory = new ActionContainerFactory(null); 343 for (Iterator iter = actions.iterator(); iter.hasNext();) { 344 Action action = (Action) iter.next(); 345 popupMenu.add(factory.createMenuItem(action)); 346 } 347 } 348 349 /** 350 * @return 351 */ 352 private Object[] getColumnControlActionKeys() { 353 Object[] allKeys = table.getActionMap().allKeys(); 354 List columnKeys = new ArrayList(); 355 for (int i = 0; i < allKeys.length; i++) { 356 if (isColumnControlActionKey(allKeys[i])) { 357 columnKeys.add(allKeys[i]); 358 } 359 } 360 return columnKeys.toArray(); 361 } 362 363 private boolean isColumnControlActionKey(Object actionKey) { 364 return (actionKey instanceof String) && ((String) actionKey).startsWith(COLUMN_CONTROL_MARKER); 365 } 366 367 368 //--------------------------- init 369 private void installTable(JXTable table) { 370 this.table = table; 371 table.addPropertyChangeListener(columnModelChangeListener); 372 updateFromColumnModelChange(null); 373 updateFromTableEnabledChanged(); 374 } 375 376 377 /** 378 * Initialize the column control button's gui 379 */ 380 private void init() { 381 setFocusPainted(false); 382 setFocusable(false); 383 // create the popup menu 384 popupMenu = new JPopupMenu(); 385 // this is a trick to get hold of the client prop which 386 // prevents closing of the popup 387 JComboBox box = new JComboBox(); 388 Object preventHide = box.getClientProperty("doNotCancelPopup"); 389 putClientProperty("doNotCancelPopup", preventHide); 390 } 391 392 393 /** 394 * the action created for this. 395 * 396 * @param icon 397 * @return 398 */ 399 private Action createControlAction(Icon icon) { 400 Action control = new AbstractAction() { 401 402 public void actionPerformed(ActionEvent e) { 403 togglePopup(); 404 } 405 406 }; 407 control.putValue(Action.SMALL_ICON, icon); 408 return control; 409 } 410 411 // -------------------------------- listeners 412 413 private PropertyChangeListener columnModelChangeListener = new PropertyChangeListener() { 414 public void propertyChange(PropertyChangeEvent evt) { 415 if ("columnModel".equals(evt.getPropertyName())) { 416 updateFromColumnModelChange((TableColumnModel) evt.getOldValue()); 417 } else if ("enabled".equals(evt.getPropertyName())) { 418 updateFromTableEnabledChanged(); 419 } 420 } 421 }; 422 423 private TableColumnModelListener columnModelListener = new TableColumnModelListener() { 424 /** Tells listeners that a column was added to the model. */ 425 public void columnAdded(TableColumnModelEvent e) { 426 // quickfix for #192 427 if (!isVisibilityChange(e, true)) { 428 populatePopupMenu(); 429 } 430 } 431 432 /** Tells listeners that a column was removed from the model. */ 433 public void columnRemoved(TableColumnModelEvent e) { 434 if (!isVisibilityChange(e, false)) { 435 populatePopupMenu(); 436 } 437 } 438 439 /** 440 * check if the add/remove event is triggered by a move to/from the 441 * invisible columns. 442 * 443 * PRE: the event must be received in columnAdded/Removed. 444 * 445 * @param e 446 * the received event 447 * @param added 448 * if true the event is assumed to be received via 449 * columnAdded, otherwise via columnRemoved. 450 * @return 451 */ 452 private boolean isVisibilityChange(TableColumnModelEvent e, 453 boolean added) { 454 // can't tell 455 if (!(e.getSource() instanceof DefaultTableColumnModelExt)) 456 return false; 457 DefaultTableColumnModelExt model = (DefaultTableColumnModelExt) e 458 .getSource(); 459 if (added) { 460 return model.isAddedFromInvisibleEvent(e.getToIndex()); 461 } else { 462 return model.isRemovedToInvisibleEvent(e.getFromIndex()); 463 } 464 } 465 466 /** Tells listeners that a column was repositioned. */ 467 public void columnMoved(TableColumnModelEvent e) { 468 } 469 470 /** Tells listeners that a column was moved due to a margin change. */ 471 public void columnMarginChanged(ChangeEvent e) { 472 } 473 474 /** 475 * Tells listeners that the selection model of the TableColumnModel 476 * changed. 477 */ 478 public void columnSelectionChanged(ListSelectionEvent e) { 479 } 480 }; 481 482 483 484 } // end class ColumnControlButton