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