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