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