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