swingx-master/swingx-core/src/main/java/org/jdesktop/swingx/table/ColumnControlButton.java
/*
 * $Id$
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx.table;

import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ItemEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.plaf.UIResource;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.action.AbstractActionExt;
import org.jdesktop.swingx.action.ActionContainerFactory;
import org.jdesktop.swingx.plaf.ColumnControlButtonAddon;
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
import org.jdesktop.swingx.table.ColumnControlPopup.ActionGrouper;
import org.jdesktop.swingx.table.ColumnControlPopup.ActionGroupable;

/**
 * A component to allow interactive customization of <code>JXTable</code>'s
 * columns. 
 * It's main purpose is to allow toggling of table columns' visibility. 
 * Additionally, arbitrary configuration actions can be exposed.
 * <p>
 * 
 * This component is installed in the <code>JXTable</code>'s
 * trailing corner, if enabled:
 * 
 * <pre><code>
 * table.setColumnControlVisible(true);
 * </code></pre>
 * 
 * From the perspective of a <code>JXTable</code>, the component's behaviour is
 * opaque. Typically, the button's action is to popup a component for user
 * interaction. <p>
 * 
 * This class is responsible for handling/providing/updating the lists of
 * actions and to keep each Action's state in synch with Table-/Column state. 
 * The visible behaviour of the popup is delegated to a
 * <code>ColumnControlPopup</code>. <p>
 * 
 * Default support for adding table (configuration or other) <code>Action</code>s is 
 * informal, driven by convention: 
 * <ul>
 * <li> the JXTable's actionMap is scanned for candidate actions, the default marker
 *   is a key of type String which starts with {@link ColumnControlButton.COLUMN_CONTROL_MARKER} 
 * <li> the actions are sorted by that key and then handed over to the ColumnControlPopup
 *   for binding and addition of appropriate menu items
 * <li> the addition as such is control by additionalActionsVisible property, its 
 *   default value is true  
 * </ul> 
 * 
 * 
 * 
 * @see TableColumnExt
 * @see TableColumnModelExt
 * @see JXTable#setColumnControl
 * 
 */
public class ColumnControlButton extends JButton {
    
    // JW: really want to extend? for builders?
    /** Marker to auto-recognize actions which should be added to the popup. */
    public static final String COLUMN_CONTROL_MARKER = "column.";
    
    /** the key for looking up the control's icon in the UIManager. Typically, it's LAF dependent. */
    public static final String COLUMN_CONTROL_BUTTON_ICON_KEY = "ColumnControlButton.actionIcon";
    
    /** the key for looking up the control's margin in the UIManager. Typically, it's LAF dependent. */
    public static final String COLUMN_CONTROL_BUTTON_MARGIN_KEY = "ColumnControlButton.margin";

    static {
        LookAndFeelAddons.contribute(new ColumnControlButtonAddon());
    }

    /** exposed for testing. */
    protected ColumnControlPopup popup;
    // TODO: the table reference is a potential leak?
    /** The table which is controlled by this. */
    private JXTable table;
    /** Listener for table property changes. */
    private PropertyChangeListener tablePropertyChangeListener;
    /** Listener for table's columnModel. */
    TableColumnModelListener columnModelListener;
    /** the list of actions for column menuitems.*/
    private List<ColumnVisibilityAction> columnVisibilityActions;

    private boolean additionalActionsVisible;

    
    /**
     * Creates a column control button for the table. Uses the default
     * icon as provided by the addon.
     * 
     * @param table  the <code>JXTable</code> controlled by this component
     */
    public ColumnControlButton(JXTable table) {
        this(table, null);
    }

    /**
     * Creates a column control button for the table. The button
     * uses the given icon and has no text.
     * @param table  the <code>JXTable</code> controlled by this component
     * @param icon the <code>Icon</code> to show
     */
    public ColumnControlButton(JXTable table, Icon icon) {
        super();
        init();
        // JW: icon LF dependent?
        setAction(createControlAction(icon));
        updateActionUI();
        updateButtonUI();
        installTable(table);
    }

    
    @Override
    public void updateUI() {
        super.updateUI();
        // JW: icon may be LF dependent
        updateActionUI();
        updateButtonUI();
        getColumnControlPopup().updateUI();
    }

    /**
     * Updates this button's properties provided by the LAF.
     * Here: overwrites the action's small_icon with the icon from the ui if the current
     *   icon is null or a UIResource.
     */
    protected void updateButtonUI() {
        if ((getMargin() == null) || (getMargin() instanceof UIResource)) {
            Insets insets = UIManager.getInsets(COLUMN_CONTROL_BUTTON_MARGIN_KEY);
            setMargin(insets); 
        }
    }
    
    /**
     * Updates the action properties provided by the LAF.
     * Here: overwrites the action's small_icon with the icon from the ui if the current
     *   icon is null or a UIResource.
     */
    protected void updateActionUI() {
        if (getAction() == null) return;
        Icon icon = (Icon) getAction().getValue(Action.SMALL_ICON);
        if ((icon == null) || (icon instanceof UIResource)) {
            icon = UIManager.getIcon(COLUMN_CONTROL_BUTTON_ICON_KEY);
            getAction().putValue(Action.SMALL_ICON, icon);
        }
    }

    /** 
     * Toggles the popup component's visibility. This method is
     * called by this control's default action. <p>
     * 
     * Here: delegates to getControlPopup().
     */ 
    public void togglePopup() {
        getColumnControlPopup().toggleVisibility(this);
    }

    /**
     * Returns the actionsVisible property which controls whether or not
     * additional table Actions should be included into the popup.
     * 
     * @return a boolean indicating whether or not additional table Actions
     *    are visible
     */
    public boolean getAdditionalActionsVisible() {
        return additionalActionsVisible;
    }
    
    
    /**
     * Sets the additonalActionsVisible property. It controls whether or
     * not additional table actions should be included into the popup. <p>
     * 
     * The default value is <code>true</code>.
     * 
     * @param additionalActionsVisible the additionalActionsVisible to set
     */
    public void setAdditionalActionsVisible(boolean additionalActionsVisible) {
        if (additionalActionsVisible == getAdditionalActionsVisible()) return;
        boolean old = getAdditionalActionsVisible();
        this.additionalActionsVisible = additionalActionsVisible;
        populatePopup();
        firePropertyChange("additionalActionsVisible", old, getAdditionalActionsVisible());
    }

    /**
     * Sets the grouper to use for grouping the additional actions. Maybe null to 
     * have no additional grouping. Has no effect
     * if the ColumnControlPopup doesn't implement Groupable. The default 
     * ColumnControlPopup supports Groupable, but is instantiated without a Grouper.
     * 
     * @param grouper
     */
    public void setActionGrouper(ActionGrouper grouper) {
        if (!(getColumnControlPopup() instanceof ActionGroupable)) return;
        ((ActionGroupable) getColumnControlPopup()).setActionGrouper(grouper);
        populatePopup();
    }
    
    @Override
    public void applyComponentOrientation(ComponentOrientation o) {
        super.applyComponentOrientation(o);
        getColumnControlPopup().applyComponentOrientation(o);
    }

   
//-------------------------- Action in synch with column properties
    /**
     * A specialized <code>Action</code> which takes care of keeping in synch with
     * TableColumn state.
     * 
     * NOTE: client must call releaseColumn if this action is no longer needed!
     * 
     */
    public class ColumnVisibilityAction extends AbstractActionExt {

        private TableColumn column;

        private PropertyChangeListener columnListener;

        /** flag to distinguish selection changes triggered by
         *  column's property change from those triggered by
         *  user interaction. Hack around #212-swingx.
         */
        private boolean fromColumn;

        /**
         * Creates a action synched to the table column.
         * 
         * @param column the <code>TableColumn</code> to keep synched to.
         */
        public ColumnVisibilityAction(TableColumn column) {
            super((String) null);
            setStateAction();
            installColumn(column);
        }

        /**
         * Releases all references to the synched <code>TableColumn</code>. 
         * Client code must call this method if the
         * action is no longer needed. After calling this action must not be
         * used any longer.
         */
        public void releaseColumn() {
            column.removePropertyChangeListener(columnListener);
            column = null;
        }
        
        /**
         * Returns true if the action is enabled. Returns
         * true only if the action is enabled and the table
         * column can be controlled.
         * 
         * @return true if the action is enabled, false otherwise
         * @see #canControlColumn()
         */
        @Override
        public boolean isEnabled() {
            return super.isEnabled() && canControlColumn(); 
        }
        
        /**
         * Returns flag to indicate if column's visibility can
         * be controlled. Minimal requirement is that column is of type 
         * <code>TableColumnExt</code>. 
         * 
         * @return boolean to indicate if columns's visibility can be controlled.
         */
        protected boolean canControlColumn() {
            // JW: should have direction? control is from action to column, the
            // other way round should be guaranteed always
            return (column instanceof TableColumnExt);
        }

        @Override
        public void itemStateChanged(final ItemEvent e) {
            if (canControlColumn()) {
                if ((e.getStateChange() == ItemEvent.DESELECTED)
                        //JW: guarding against 1 leads to #212-swingx: setting
                        // column visibility programatically fails if
                        // the current column is the second last visible
                        // guarding against 0 leads to hiding all columns
                        // by deselecting the menu item. 
                        && (table.getColumnCount() <= 1)
                        // JW Fixed #212: basically implemented Rob's idea to distinguish
                        // event sources instead of unconditionally reselect
                        // not entirely sure if the state transitions are completely
                        // defined but all related tests are passing now.
                        && !fromColumn) {
                    reselect();
                } else {
                    setSelected(e.getStateChange() == ItemEvent.SELECTED);
                }
            }
        }

        
        @Override
        public synchronized void setSelected(boolean newValue) {
            super.setSelected(newValue);
            if (canControlColumn()) {
                if (!fromColumn)
                ((TableColumnExt) column).setVisible(newValue);
            }
        }

        /**
         * Does nothing. Synch from action state to TableColumn state
         * is done in itemStateChanged.
         */
        @Override
        public void actionPerformed(ActionEvent e) {

        }
        
        /**
         * Synchs selected property to visible. This
         * is called on change of tablecolumn's <code>visible</code> property.
         * 
         * @param visible column visible state to synch to.
         */
        private void updateFromColumnVisible(boolean visible) {
//            /*boolean*/ visible = true;
//            if (canControlColumn()) {
//                visible = ((TableColumnExt) column).isVisible();
//            }
            fromColumn = true;
            setSelected(visible);
            fromColumn = false;
        }


        protected void updateFromColumnHideable(boolean hideable) {
            setEnabled(hideable);
        }

        /**
         * Synchs name property to value. This is called on change of 
         * tableColumn's <code>headerValue</code> property.
         * 
         * @param value
         */
        private void updateFromColumnHeader(Object value) {
            setName(String.valueOf(value));
        }

        /**
         * Enforces selected to <code>true</code>. Called if user interaction
         * tried to de-select the last single visible column.
         *
         */
        private void reselect() {
            firePropertyChange("selected", null, Boolean.TRUE);
        }

        // -------------- init
        private void installColumn(TableColumn column) {
            this.column = column;
            column.addPropertyChangeListener(getColumnListener());
            updateFromColumnHeader(column.getHeaderValue());
            // #429-swing: actionCommand must be string
            if (column.getIdentifier() != null) {
                setActionCommand(column.getIdentifier().toString());
            }
            boolean visible = (column instanceof TableColumnExt) ? 
                    ((TableColumnExt) column).isVisible() : true;
            updateFromColumnVisible(visible);
            boolean hideable = (column instanceof TableColumnExt) ? 
                    ((TableColumnExt) column).isHideable() : true;
            updateFromColumnHideable(hideable);        
        }

        /**
         * Returns the listener to column's property changes. The listener
         * is created lazily if necessary.
         * 
         * @return the <code>PropertyChangeListener</code> listening to 
         *   <code>TableColumn</code>'s property changes, guaranteed to be 
         *   not <code>null</code>.
         */
        protected PropertyChangeListener getColumnListener() {
            if (columnListener == null) {
                columnListener = createPropertyChangeListener();
            }
            return columnListener;
        }

        /**
         * Creates and returns the listener to column's property changes.
         * Subclasses are free to roll their own.
         * <p>
         * Implementation note: this listener reacts to column's
         * <code>visible</code> and <code>headerValue</code> properties and
         * calls the respective <code>updateFromXX</code> methodes.
         * 
         * @return the <code>PropertyChangeListener</code> to use with the
         *         column
         */
        protected PropertyChangeListener createPropertyChangeListener() {
            return new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    if ("visible".equals(evt.getPropertyName())) {
                        updateFromColumnVisible((Boolean) evt.getNewValue());
                    } else if ("headerValue".equals(evt.getPropertyName())) {
                        updateFromColumnHeader(evt.getNewValue());
                    } else if ("hideable".equals(evt.getPropertyName())) {
                        updateFromColumnHideable((Boolean) evt.getNewValue());
                    }
                }
            };
        }
    }

    // ---------------------- the popup

    /**
     * A default implementation of ColumnControlPopup. 
     * It uses a JPopupMenu with 
     * MenuItems corresponding to the Actions as 
     * provided by the ColumnControlButton.
     * 
     * 
     */
    public class DefaultColumnControlPopup implements ColumnControlPopup, ActionGroupable {
        private JPopupMenu popupMenu;
        private ActionGrouper grouper;

        public DefaultColumnControlPopup() {
            this(null);
        }
        
        //------------------ public methods to control visibility status
        
        public DefaultColumnControlPopup(ActionGrouper grouper) {
            this.grouper = grouper;
        }

        /** 
         * @inheritDoc
         * 
         */
        @Override
        public void updateUI() {
            SwingUtilities.updateComponentTreeUI(getPopupMenu());
        }

        /** 
         * @inheritDoc
         * 
         */
        @Override
        public void toggleVisibility(JComponent owner) {
            JPopupMenu popupMenu = getPopupMenu();
            if (popupMenu.isVisible()) {
                popupMenu.setVisible(false);
            } else if (popupMenu.getComponentCount() > 0) {
                Dimension buttonSize = owner.getSize();
                int xPos = owner.getComponentOrientation().isLeftToRight() ? buttonSize.width
                        - popupMenu.getPreferredSize().width
                        : 0;
                popupMenu.show(owner,
                        // JW: trying to allow popup without CCB showing
                        // weird behaviour
//                        owner.isShowing()? owner : null, 
                        xPos, buttonSize.height);
            }

        }

        /** 
         * @inheritDoc
         * 
         */
        @Override
        public void applyComponentOrientation(ComponentOrientation o) {
            getPopupMenu().applyComponentOrientation(o);

        }

        //-------------------- public methods to manipulate popup contents.
        
        /** 
         * @inheritDoc
         * 
         */
        @Override
        public void removeAll() {
            getPopupMenu().removeAll();
        }


        /** 
         * @inheritDoc
         * 
         */
        @Override
        public void addVisibilityActionItems(
                List<? extends AbstractActionExt> actions) {
            addItems(new ArrayList<Action>(actions));

        }


        /** 
         * @inheritDoc
         * 
         */
        @Override
        public void addAdditionalActionItems(List<? extends Action> actions) {
            if (actions.size() == 0)
                return;
            // JW: this is a reference to the enclosing class 
            // prevents to make this implementation static
            // Hmmm...any way around?
            if (canControl()) {
                addSeparator();
            }
            
            if (getGrouper() == null) {
                addItems(actions);
                return;
            }
            List<? extends List<? extends Action>> groups = grouper.group(actions);
            for (List<? extends Action> group : groups) {
                addItems(group);
                if (group != groups.get(groups.size()- 1))
                    addSeparator();
                
            }
            
        }
        
        //--------------------------- internal helpers to manipulate popups content
        
        /**
         * Here: creates and adds a menuItem to the popup for every 
         * Action in the list. Does nothing if 
         * if the list is empty.
         * 
         * PRE: actions != null.
         * 
         * @param actions a list containing the actions to add to the popup.
         *        Must not be null.
         * 
         */
        protected void addItems(List<? extends Action> actions) {
            ActionContainerFactory factory = new ActionContainerFactory(null);
            for (Action action : actions) {
                addItem(factory.createMenuItem(action));
            }
        }
        
        /**
         * adds a separator to the popup.
         *
         */
        protected void addSeparator() {
            getPopupMenu().addSeparator();
        }

        /**
         * 
         * @param item the menuItem to add to the popup.
         */
        protected void addItem(JMenuItem item) {
            getPopupMenu().add(item);
        }

        /**
         * 
         * @return the popup to add menuitems. Guaranteed to be != null.
         */
        protected JPopupMenu getPopupMenu() {
            if (popupMenu == null) {
                popupMenu = new JPopupMenu();
            }
            return popupMenu;
        }

        // --------------- implement Groupable
        
        @Override
        public void setActionGrouper(ActionGrouper grouper) {
            this.grouper = grouper;
        }
        
        protected ActionGrouper getGrouper() {
            return grouper;
        }

    }
    /**
     * Returns to popup component for user interaction. Lazily 
     * creates the component if necessary.
     * 
     * @return the ColumnControlPopup for showing the items, guaranteed
     *   to be not <code>null</code>.
     * @see #createColumnControlPopup()  
     */
    protected ColumnControlPopup getColumnControlPopup() {
        if (popup == null) {
            popup = createColumnControlPopup();
        }
        return popup;
    }

    /**
     * Factory method to return a <code>ColumnControlPopup</code>.
     * Subclasses can override to hook custom implementations.
     * 
     * @return the <code>ColumnControlPopup</code> used.
     */
    protected ColumnControlPopup createColumnControlPopup() {
        return new DefaultColumnControlPopup();
    }


//-------------------------- updates from table propertyChangelistnere
    
    /**
     * Adjusts internal state after table's column model property has changed.
     * Handles cleanup of listeners to the old/new columnModel (Note, that
     * it listens to the column model only if it can control column visibility).
     * Updates content of popup.
     * 
     * @param oldModel the old <code>TableColumnModel</code> we had been listening to.
     */
    protected void updateFromColumnModelChange(TableColumnModel oldModel) {
        if (oldModel != null) {
            oldModel.removeColumnModelListener(columnModelListener);
        }
        populatePopup();
        if (canControl()) {
            table.getColumnModel().addColumnModelListener(getColumnModelListener());
        }
    }
    
    /**
     * Synchs this button's enabled with table's enabled.
     *
     */
    protected void updateFromTableEnabledChanged() {
        getAction().setEnabled(table.isEnabled());
        
    }
    /**
     * Method to check if we can control column visibility POST: if true we can
     * be sure to have an extended TableColumnModel
     * 
     * @return boolean to indicate if controlling the visibility state is
     *   possible. 
     */
    protected boolean canControl() {
        return table.getColumnModel() instanceof TableColumnModelExt;
    }
 
//  ------------------------ updating the popup
    /**
     * Populates the popup from scratch.
     * 
     * If applicable, creates and adds column visibility actions. Always adds
     * additional actions.
     */
    protected void populatePopup() {
        clearAll();
        if (canControl()) {
            createVisibilityActions();
            addVisibilityActionItems();
        }
        addAdditionalActionItems();
    }

    /**
     * 
     * removes all components from the popup, making sure to release all
     * columnVisibility actions.
     * 
     */
    protected void clearAll() {
        clearColumnVisibilityActions();
        getColumnControlPopup().removeAll();
    }


    /**
     * Releases actions and clears list of actions.
     * 
     */
    protected void clearColumnVisibilityActions() {
        if (columnVisibilityActions == null)
            return;
        for (ColumnVisibilityAction action : columnVisibilityActions) {
            action.releaseColumn();
        }
        columnVisibilityActions.clear();
    }

   
    /**
     * Adds visibility actions into the popup view.
     * 
     * Here: delegates the list of actions to the DefaultColumnControlPopup.
     * <p>
     * PRE: columnVisibilityActions populated before calling this.
     * 
     */
    protected void addVisibilityActionItems() {
        getColumnControlPopup().addVisibilityActionItems(
                Collections.unmodifiableList(getColumnVisibilityActions()));
    }

    /**
     * Adds additional actions to the popup, if additionalActionsVisible is true,
     * does nothing otherwise.<p>
     * 
     * Here: delegates the list of actions as returned by #getAdditionalActions() 
     *   to the DefaultColumnControlPopup. 
     * Does nothing if #getColumnActions() is empty.
     * 
     */
    protected void addAdditionalActionItems() {
        if (!getAdditionalActionsVisible()) return;
        getColumnControlPopup().addAdditionalActionItems(
                Collections.unmodifiableList(getAdditionalActions()));
    }


    /**
     * Creates and adds a ColumnVisiblityAction for every column that should be
     * togglable via the column control. <p>
     * 
     * Here: all table columns contained in the <code>TableColumnModel</code> - 
     * visible and invisible columns - to <code>createColumnVisibilityAction</code> and
     * adds all not <code>null</code> return values.
     * 
     * <p>
     * PRE: canControl()
     * 
     * @see #createColumnVisibilityAction
     */
    protected void createVisibilityActions() {
        List<TableColumn> columns = table.getColumns(true);
        for (TableColumn column : columns) {
            ColumnVisibilityAction action = createColumnVisibilityAction(column);
            if (action != null) {
                getColumnVisibilityActions().add(action);
            }
        }

    }

    /**
     * Creates and returns a <code>ColumnVisibilityAction</code> for the given 
     * <code>TableColumn</code>. The return value might be null, f.i. if the
     * column should not be allowed to be toggled.
     * 
     * @param column the <code>TableColumn</code> to use for the action
     * @return a ColumnVisibilityAction to use for the given column, 
     *    may be <code>null</code>.
     */
    protected ColumnVisibilityAction createColumnVisibilityAction(TableColumn column) {
        return new ColumnVisibilityAction(column);
    }

    /**
     * Lazyly creates and returns the List of visibility actions.
     * 
     * @return the list of visibility actions, guaranteed to be != null.
     */
    protected List<ColumnVisibilityAction> getColumnVisibilityActions() {
        if (columnVisibilityActions == null) {
            columnVisibilityActions = new ArrayList<ColumnVisibilityAction>();
        }
        return columnVisibilityActions;
    }


    /**
     * creates and returns a list of additional Actions to add to the popup.
     * Here: the actions are looked up in the table's actionMap according
     * to the keys as returned from #getColumnControlActionKeys();
     * 
     * @return a list containing all additional actions to include into the popup.
     */
    protected List<Action> getAdditionalActions() {
        List<?> actionKeys = getColumnControlActionKeys();
        List<Action> actions = new ArrayList<Action>();
        for (Object key : actionKeys) {
          actions.add(table.getActionMap().get(key));
        }
        return actions;
    }

    /**
     * Looks up and returns action keys to access actions in the 
     * table's actionMap which should be included into the popup.
     * 
     * Here: all keys with isColumnControlActionKey(key). The list 
     * is sorted by those keys.
     * 
     * @return the action keys of table's actionMap entries whose
     *   action should be included into the popup.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected List getColumnControlActionKeys() {
        Object[] allKeys = table.getActionMap().allKeys();
        List columnKeys = new ArrayList();
        for (int i = 0; i < allKeys.length; i++) {
            if (isColumnControlActionKey(allKeys[i])) {
                columnKeys.add(allKeys[i]);
            }
        }
        // JW: this will blow for non-String keys!
        // so this method is less decoupled from the
        // decision method isControl than expected. 
        Collections.sort(columnKeys);
        return columnKeys;
    }

    /**
     * Here: true if a String key starts with #COLUMN_CONTROL_MARKER.
     * 
     * @param actionKey a key in the table's actionMap.
     * @return a boolean to indicate whether the given actionKey maps to
     *    an action which should be included into the popup.
     *    
     */
    protected boolean isColumnControlActionKey(Object actionKey) {
        return (actionKey instanceof String) && 
            ((String) actionKey).startsWith(COLUMN_CONTROL_MARKER);
    }


    //--------------------------- init

    private void installTable(JXTable table) {
        this.table = table;
        table.addPropertyChangeListener(getTablePropertyChangeListener());
        updateFromColumnModelChange(null);
        updateFromTableEnabledChanged();
    }


    /**
     * Initialize the column control button's gui
     */
    private void init() {
        setFocusPainted(false);
        setFocusable(false);
        // this is a trick to get hold of the client prop which
        // prevents closing of the popup
        JComboBox box = new JComboBox();
        Object preventHide = box.getClientProperty("doNotCancelPopup");
        putClientProperty("doNotCancelPopup", preventHide);
        additionalActionsVisible = true;
    }


    /** 
     * Creates and returns the default action for this button.
     * @param icon 
     * 
     * @param icon the Icon to use in the action.
     * @return the default action.
     */
    private Action createControlAction(Icon icon) {
        
        Action control = new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                togglePopup();
            }

        };
        control.putValue(Action.SMALL_ICON, icon);
        return control;
    }
    
    // -------------------------------- listeners

    /**
     * Returns the listener to table's property changes. The listener is 
     * lazily created if necessary. 
     * @return the <code>PropertyChangeListener</code> for use with the 
     *  table, guaranteed to be not <code>null</code>.
     */
    protected PropertyChangeListener getTablePropertyChangeListener() {
        if (tablePropertyChangeListener == null) {
            tablePropertyChangeListener = createTablePropertyChangeListener();
        }
        return tablePropertyChangeListener;
    }

    /**
     * Creates the listener to table's property changes. Subclasses are free
     * to roll their own. <p>
     * Implementation note: this listener reacts to table's <code>enabled</code> and
     * <code>columnModel</code> properties and calls the respective 
     * <code>updateFromXX</code> methodes.
     * 
     * @return the <code>PropertyChangeListener</code> for use with the table.
     */
    protected PropertyChangeListener createTablePropertyChangeListener() {
        return new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if ("columnModel".equals(evt.getPropertyName())) {
                    updateFromColumnModelChange((TableColumnModel) evt
                            .getOldValue());
                } else if ("enabled".equals(evt.getPropertyName())) {
                    updateFromTableEnabledChanged();
                }
            }
        };
    }

    /**
     * Returns the listener to table's column model. The listener is 
     * lazily created if necessary. 
     * @return the <code>TableColumnModelListener</code> for use with the 
     *  table's column model, guaranteed to be not <code>null</code>.
     */
    protected TableColumnModelListener getColumnModelListener() {
        if (columnModelListener == null) {
            columnModelListener = createColumnModelListener();
        }
        return columnModelListener;
    }
    
    /**
     * Creates the listener to columnModel. Subclasses are free to roll their
     * own.
     * <p>
     * Implementation note: this listener reacts to "real" columnRemoved/-Added by
     * populating the popups content from scratch.
     * 
     * @return the <code>TableColumnModelListener</code> for use with the
     *         table's columnModel.
     */
    protected TableColumnModelListener createColumnModelListener() {
        return new TableColumnModelListener() {
            /** Tells listeners that a column was added to the model. */
            @Override
            public void columnAdded(TableColumnModelEvent e) {
                // quickfix for #192
                if (!isVisibilityChange(e, true)) {
                    populatePopup();
                }
            }

            /** Tells listeners that a column was removed from the model. */
            @Override
            public void columnRemoved(TableColumnModelEvent e) {
                if (!isVisibilityChange(e, false)) {
                    populatePopup();
                }
            }

            /**
             * Check if the add/remove event is triggered by a move to/from the
             * invisible columns. 
             * 
             * PRE: the event must be received in columnAdded/Removed.
             * 
             * @param e the received event
             * @param added if true the event is assumed to be received via
             *        columnAdded, otherwise via columnRemoved.
             * @return boolean indicating whether the removed/added is a side-effect
             *    of hiding/showing the column.
             */
            private boolean isVisibilityChange(TableColumnModelEvent e,
                    boolean added) {
                // can't tell
                if (!(e.getSource() instanceof DefaultTableColumnModelExt))
                    return false;
                DefaultTableColumnModelExt model = (DefaultTableColumnModelExt) e
                        .getSource();
                if (added) {
                    return model.isAddedFromInvisibleEvent(e.getToIndex());
                } else {
                    return model.isRemovedToInvisibleEvent(e.getFromIndex());
                }
            }

            /** Tells listeners that a column was repositioned. */
            @Override
            public void columnMoved(TableColumnModelEvent e) {
            }

            /** Tells listeners that a column was moved due to a margin change. */
            @Override
            public void columnMarginChanged(ChangeEvent e) {
            }

            /**
             * Tells listeners that the selection model of the TableColumnModel
             * changed.
             */
            @Override
            public void columnSelectionChanged(ListSelectionEvent e) {
            }
        };
    }


} // end class ColumnControlButton
