001/*
002 * $Id: JXTableHeader.java 3960 2011-03-15 19:36:53Z kschaefe $
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 */
021package org.jdesktop.swingx;
022
023import java.awt.Component;
024import java.awt.Dimension;
025import java.awt.event.MouseEvent;
026import java.beans.PropertyChangeEvent;
027import java.beans.PropertyChangeListener;
028import java.io.Serializable;
029import java.util.logging.Logger;
030
031import javax.swing.JTable;
032import javax.swing.SortOrder;
033import javax.swing.SwingUtilities;
034import javax.swing.event.MouseInputListener;
035import javax.swing.table.JTableHeader;
036import javax.swing.table.TableCellRenderer;
037import javax.swing.table.TableColumn;
038import javax.swing.table.TableColumnModel;
039
040import org.jdesktop.swingx.event.TableColumnModelExtListener;
041import org.jdesktop.swingx.plaf.LookAndFeelAddons;
042import org.jdesktop.swingx.plaf.TableHeaderAddon;
043import org.jdesktop.swingx.sort.SortController;
044import org.jdesktop.swingx.table.TableColumnExt;
045
046/**
047 * TableHeader with extended functionality if associated Table is of
048 * type JXTable.<p>
049 * 
050 * <h2> Extended user interaction </h2>
051 * 
052 * <ul>
053 * <li> Supports column pack (== auto-resize to exactly fit the contents)
054 *  on double-click in resize region.
055 *  <li> Configurable to resort a column on the second click of a mouseClicked event
056 *  (feature request #271-swingx)
057 * <li> Does its best to not sort if the mouse click happens in the resize region.
058 *  <li> Supports horizontal auto-scroll if a column is dragged outside visible rectangle. 
059 *  This feature is enabled if the autoscrolls property is true. The default is false 
060 *  (because of Issue #788-swingx which still isn't fixed for jdk1.6).
061 * </ul>
062 * 
063 * Note: extended sort and resize related functionality is fully effective only if the header's
064 * table is of type JXTable and has control over the row sorter, that is the row sorter
065 * is of type SortController.
066 * 
067 * <h2> Extended functionality </h2>
068 * 
069 * <ul>
070 * <li> Listens to TableColumn propertyChanges to update itself accordingly.
071 * <li> Supports per-column header ToolTips. 
072 * <li> Guarantees reasonable minimal height > 0 for header preferred height.
073* </ul>
074 * 
075 * 
076 * @author Jeanette Winzenburg
077 * 
078 * @see JXTable#toggleSortOrder(int)
079 * @see JXTable#resetSortOrder()
080 * @see SortGestureRecognizer
081 */
082public class JXTableHeader extends JTableHeader 
083    implements TableColumnModelExtListener {
084
085    @SuppressWarnings("unused")
086    private static final Logger LOG = Logger.getLogger(JXTableHeader.class
087            .getName());
088    
089    static {
090        LookAndFeelAddons.contribute(new TableHeaderAddon());
091    }
092
093    private transient PropertyChangeListener tablePropertyChangeListener;
094    private boolean resortsOnDoubleClick;
095
096    /**
097     *  Constructs a <code>JTableHeader</code> with a default 
098     *  <code>TableColumnModel</code>.
099     *
100     * @see #createDefaultColumnModel
101     */
102    public JXTableHeader() {
103        super();
104    }
105
106    /**
107     * Constructs a <code>JTableHeader</code> which is initialized with
108     * <code>cm</code> as the column model. If <code>cm</code> is
109     * <code>null</code> this method will initialize the table header with a
110     * default <code>TableColumnModel</code>.
111     * 
112     * @param columnModel the column model for the table
113     * @see #createDefaultColumnModel
114     */
115    public JXTableHeader(TableColumnModel columnModel) {
116        super(columnModel);
117    }
118
119
120    /**
121     * {@inheritDoc} <p>
122     * Sets the associated JTable. Enables enhanced header
123     * features if table is of type JXTable.<p>
124     * 
125     * PENDING: who is responsible for synching the columnModel?
126     */
127    @Override
128    public void setTable(JTable table) {
129        uninstallTable();
130        super.setTable(table);
131        installTable();
132//        setColumnModel(table.getColumnModel());
133        // the additional listening option makes sense only if the table
134        // actually is a JXTable
135        if (getXTable() != null) {
136            installHeaderListener();
137        } else {
138            uninstallHeaderListener();
139        }
140    }
141
142    /**
143     * Installs the table. <p>
144     * This implemenation synchs enabled state and installs the PropertyChangeListener. 
145     */
146    protected void installTable() {
147        updateEnabledFromTable();
148        if (getTable() == null) return;
149        getTable().addPropertyChangeListener(getTablePropertyChangeListener());
150    }
151    
152    /**
153     * Synchs the header's enabled with the table's enabled property.
154     */
155    protected void updateEnabledFromTable() {
156        setEnabled(getTable() != null ? getTable().isEnabled() : true);
157    }
158
159    /**
160     * Uninstalls the table. <p>
161     * This implementation uninstalls the PropertyChangeListener.
162     */
163    protected void uninstallTable() {
164        if (getTable() == null) return;
165        getTable().removePropertyChangeListener(getTablePropertyChangeListener());
166    }
167
168
169    /**
170     * Implements TableColumnModelExt to allow internal update after
171     * column property changes.<p>
172     * 
173     * This implementation triggers a resizeAndRepaint on every propertyChange which
174     * doesn't already fire a "normal" columnModelEvent.
175     * 
176     * @param event change notification from a contained TableColumn.
177     * @see #isColumnEvent(PropertyChangeEvent)
178     * @see TableColumnModelExtListener
179     * 
180     * 
181     */
182    @Override
183    public void columnPropertyChange(PropertyChangeEvent event) {
184       if (isColumnEvent(event)) return;
185       resizeAndRepaint(); 
186    }
187    
188    
189    /**
190     * Returns a boolean indicating if a property change event received
191     * from column changes is expected to be already broadcasted by the
192     * core TableColumnModel. <p>
193     * 
194     * This implementation returns true for notification of width, preferredWidth
195     * and visible properties, false otherwise.
196     * 
197     * @param event the PropertyChangeEvent received as TableColumnModelExtListener.
198     * @return a boolean to decide whether the same event triggers a
199     *   base columnModelEvent.
200     */
201    protected boolean isColumnEvent(PropertyChangeEvent event) {
202        return "width".equals(event.getPropertyName()) || 
203            "preferredWidth".equals(event.getPropertyName())
204            || "visible".equals(event.getPropertyName());
205    }
206
207    /**
208     * {@inheritDoc} <p>
209     * 
210     * Overridden to respect the column tooltip, if available. 
211     * 
212     * @return the column tooltip of the column at the mouse position 
213     *   if not null or super if not available.
214     */
215    @Override
216    public String getToolTipText(MouseEvent event) {
217        String columnToolTipText = getColumnToolTipText(event);
218        return columnToolTipText != null ? columnToolTipText : super.getToolTipText(event);
219    }
220
221    /**
222     * Returns the column tooltip of the column at the position
223     * of the MouseEvent, if a tooltip is available.
224     * 
225     * @param event the mouseEvent representing the mouse location.
226     * @return the column tooltip of the column below the mouse location,
227     *   or null if not available.
228     */
229    protected String getColumnToolTipText(MouseEvent event) {
230        if (getXTable() == null) return null;
231        int column = columnAtPoint(event.getPoint());
232        if (column < 0) return null;
233        TableColumnExt columnExt = getXTable().getColumnExt(column);
234        return columnExt != null ? columnExt.getToolTipText() : null;
235    }
236    
237    /**
238     * Returns the associated table if it is of type JXTable, or null if not.
239     * 
240     * @return the associated table if of type JXTable or null if not.
241     */
242    public JXTable getXTable() {
243        if (!(getTable() instanceof JXTable))
244            return null;
245        return (JXTable) getTable();
246    }
247
248    /**
249     * Returns the resortsOnDoubleClick property. 
250     * 
251     * @return a flag indicating whether or not the second click in a mouseClicked
252     * event should toggle the sort order again.
253     * 
254     * @see #setResortsOnDoubleClick(boolean)
255     */
256    public boolean getResortsOnDoubleClick() {
257        return getXTable() != null && resortsOnDoubleClick;
258    }
259    
260    /**
261     * Sets the resortsOnDoubleClick property. If enabled, the second click
262     * of a mouseClicked event will toggle the sort order again if the
263     * column has been unsorted before. This is introduced to support
264     * feature request #271-swingx. It is effective only if the coupled table
265     * is of type JXTable and has full control about its RowSorter's properties.
266     * 
267     * The default value is false. 
268     * 
269     * @param resortsOnDoubleClick a boolean indicating whether or not the
270     *    second click in a mouseClicked event should resort the column.
271     *    
272     *  @see #getResortsOnDoubleClick()  
273     */
274    public void setResortsOnDoubleClick(boolean resortsOnDoubleClick) {
275        boolean old = getResortsOnDoubleClick();
276        this.resortsOnDoubleClick = resortsOnDoubleClick;
277        firePropertyChange("resortsOnDoubleClick", old, getResortsOnDoubleClick());
278    }
279    
280    /**
281     * Returns the TableCellRenderer to use for the column with the given index. This
282     * implementation returns the column's header renderer if available or this header's
283     * default renderer if not.
284     * 
285     * @param columnIndex the index in view coordinates of the column
286     * @return the renderer to use for the column, guaranteed to be not null.
287     */
288    public TableCellRenderer getCellRenderer(int columnIndex) {
289        TableCellRenderer renderer = getColumnModel().getColumn(columnIndex).getHeaderRenderer();
290        return renderer != null ? renderer : getDefaultRenderer();
291    }
292    
293    /**
294     * {@inheritDoc} <p>
295     * 
296     * Overridden to adjust for a reasonable minimum height. Done to fix Issue 334-swingx,
297     * which actually is a core issue misbehaving in returning a zero height
298     * if the first column has no text. 
299     * 
300     * @see #getPreferredSize(Dimension)
301     * @see #getMinimumHeight(int).
302     * 
303     */
304    @Override
305    public Dimension getPreferredSize() {
306        Dimension pref = super.getPreferredSize();
307        pref = getPreferredSize(pref);
308        pref.height = getMinimumHeight(pref.height);
309        return pref;
310    }
311    
312    /**
313     * Returns a preferred size which is adjusted to the maximum of all
314     * header renderers' height requirement.
315     * 
316     * @param pref an initial preferred size
317     * @return the initial preferred size with its height property adjusted 
318     *      to the maximum of all renderers preferred height requirement. 
319     *  
320     *  @see #getPreferredSize()
321     *  @see #getMinimumHeight(int)
322     */
323    protected Dimension getPreferredSize(Dimension pref) {
324        int height = pref.height;
325        for (int i = 0; i < getColumnModel().getColumnCount(); i++) {
326            TableCellRenderer renderer = getCellRenderer(i);
327            Component comp = renderer.getTableCellRendererComponent(table, 
328                    getColumnModel().getColumn(i).getHeaderValue(), false, false, -1, i);
329            height = Math.max(height, comp.getPreferredSize().height);
330        }
331        pref.height = height;
332        return pref;
333        
334    }
335
336    /**
337     * Returns a reasonable minimal preferred height for the header. This is
338     * meant as a last straw if all header values are null, renderers report 0 as
339     * their preferred height.<p>
340     * 
341     * This implementation returns the default header renderer's preferred height as measured
342     * with a dummy value if the input height is 0, otherwise returns the height
343     * unchanged.
344     * 
345     * @param height the initial height.
346     * @return a reasonable minimal preferred height.
347     * 
348     * @see #getPreferredSize()
349     * @see #getPreferredSize(Dimension)
350     */
351    protected int getMinimumHeight(int height) {
352        if ((height == 0)) {
353//                && (getXTable() != null) 
354//                && getXTable().isColumnControlVisible()){
355            TableCellRenderer renderer = getDefaultRenderer();
356            Component comp = renderer.getTableCellRendererComponent(getTable(), 
357                        "dummy", false, false, -1, -1);
358            height = comp.getPreferredSize().height;
359        }
360        return height;
361    }
362    
363
364    /**
365     * @inherited <p>
366     * 
367     * Overridden to fire a propertyChange for draggedColumn. 
368     */
369    @Override
370    public void setDraggedColumn(TableColumn column) {
371        if (getDraggedColumn() == column) return;
372        TableColumn old = getDraggedColumn();
373        super.setDraggedColumn(column);
374        firePropertyChange("draggedColumn", old, getDraggedColumn());
375    }
376
377    
378    /**
379     * @inherited <p>
380     * 
381     * Overridden to fire a propertyChange for resizingColumn. 
382     */
383    @Override
384    public void setResizingColumn(TableColumn aColumn) {
385        if (getResizingColumn() == aColumn) return;
386        TableColumn old = getResizingColumn();
387        super.setResizingColumn(aColumn);
388        firePropertyChange("resizingColumn", old, getResizingColumn());
389    }
390    
391    
392    
393    /**
394     * {@inheritDoc} <p>
395     * 
396     * Overridden to scroll the table to keep the dragged column visible.
397     * This side-effect is enabled only if the header's autoscroll property is
398     * <code>true</code> and the associated table is of type JXTable.<p> 
399     * 
400     * The autoscrolls is disabled by default. With or without - core 
401     * issue #6503981 has weird effects (for jdk 1.6 - 1.6u3) on a plain 
402     * JTable as well as a JXTable, fixed in 1.6u4.
403     * 
404     */
405    @Override
406    public void setDraggedDistance(int distance) {
407        int old = getDraggedDistance();
408        super.setDraggedDistance(distance);
409        // fire because super doesn't
410        firePropertyChange("draggedDistance", old, getDraggedDistance());
411        if (!getAutoscrolls() || (getXTable() == null)) return;
412        TableColumn column = getDraggedColumn();
413        // fix for #788-swingx: don't try to scroll if we have no dragged column
414        // as doing will confuse the horizontalScrollEnabled on the JXTable.
415        if (column != null) {
416            getXTable().scrollColumnToVisible(getViewIndexForColumn(column));
417        }
418    }
419    
420    /**
421     * Returns the the dragged column if and only if, a drag is in process and
422     * the column is visible, otherwise returns <code>null</code>.
423     * 
424     * @return the dragged column, if a drag is in process and the column is
425     *         visible, otherwise returns <code>null</code>
426     * @see #getDraggedDistance
427     */
428    @Override
429    public TableColumn getDraggedColumn() {
430        return isVisible(draggedColumn) ? draggedColumn : null; 
431    }
432
433    /**
434     * Checks and returns the column's visibility. 
435     * 
436     * @param column the <code>TableColumn</code> to check
437     * @return a boolean indicating if the column is visible
438     */
439    private boolean isVisible(TableColumn column) {
440        return getViewIndexForColumn(column) >= 0;
441    }
442
443    /**
444     * Returns the (visible) view index for the table column
445     * or -1 if not visible or not contained in this header's
446     * columnModel.
447     * 
448     * 
449     * @param aColumn the TableColumn to find the view index for
450     * @return the view index of the given table column or -1 if not visible
451     * or not contained in the column model.
452     */
453    private int getViewIndexForColumn(TableColumn aColumn) {
454        if (aColumn == null)
455            return -1;
456        TableColumnModel cm = getColumnModel();
457        for (int column = 0; column < cm.getColumnCount(); column++) {
458            if (cm.getColumn(column) == aColumn) {
459                return column;
460            }
461        }
462        return -1;
463    }
464    
465    /**
466     * Returns the PropertyChangeListener to register on the owning table,
467     * lazily created.
468     * 
469     * @return the PropertyChangeListener to use on the owning table.
470     */
471    protected PropertyChangeListener getTablePropertyChangeListener() {
472        if (tablePropertyChangeListener == null) {
473            tablePropertyChangeListener = createTablePropertyChangeListener();
474        }
475        return tablePropertyChangeListener;
476    }
477
478    /**
479     * Creates and returns the PropertyChangeListener to register on the 
480     * owning table.<p>
481     * 
482     * This implementation synchs the header's enabled properties with the 
483     * table's enabled.
484     * 
485     * @return the PropertyChangeListener to register on the owning table.
486     */
487    protected PropertyChangeListener createTablePropertyChangeListener() {
488        PropertyChangeListener l = new PropertyChangeListener() {
489            
490            @Override
491            public void propertyChange(PropertyChangeEvent evt) {
492                if ("enabled".equals(evt.getPropertyName())) {
493                    updateEnabledFromTable();
494                }
495            }
496        };
497        return l;
498    }
499
500
501    /**
502     * Creates and installs header listeners to service the extended functionality.
503     * This implementation creates and installs a custom mouse input listener.
504     */
505    protected void installHeaderListener() {
506        if (headerListener == null) {
507            headerListener = new HeaderListener();
508            addMouseListener(headerListener);
509            addMouseMotionListener(headerListener);
510        }
511    }
512
513    /**
514     * Uninstalls header listeners to service the extended functionality.
515     * This implementation uninstalls a custom mouse input listener.
516     */
517    protected void uninstallHeaderListener() {
518        if (headerListener != null) {
519            removeMouseListener(headerListener);
520            removeMouseMotionListener(headerListener);
521            headerListener = null;
522        }
523    }
524
525    private MouseInputListener headerListener;
526
527    /**
528     * A MouseListener implementation to support enhanced tableHeader functionality.
529     * 
530     * Supports column "packing" by double click in resize region. Works around
531     * core issue #6862170 (must not sort column by click into resize region).
532     * <p>
533     * 
534     * Note that the logic is critical, mostly because it must be independent of
535     * sequence of listener notification. So we check whether or not a pressed
536     * happens in the resizing region in both pressed and released, taking the
537     * header's resizingColumn property as a marker. The inResize flag can only
538     * be turned on in those. At the end of the released, we check if we are
539     * in resize and disable core sorting - which happens in clicked - if appropriate.
540     * In our clicked we hook the pack action (happens only on double click)
541     * and reset the resizing region flag always. Pressed (and all other methods)
542     * restore sorting enablement. 
543     * <p>
544     * 
545     * Supports resort on double click if enabled in the JXTableHeader (Issue #271-swingx). 
546     * 
547     * Is fully effective only if JXTable has control over the row sorter, that is
548     * if the row sorter is of type SortController.
549     * 
550     */
551    private class HeaderListener implements MouseInputListener, Serializable {
552        private TableColumn cachedResizingColumn;
553        private SortOrder[] cachedSortOrderCycle;
554        private int sortColumn = -1;
555        
556        /**
557         * Packs column on double click in resize region. Resorts 
558         * column on double click if enabled and not in resize region.
559         */
560        @Override
561        public void mouseClicked(MouseEvent e) {
562            if (shouldIgnore(e)) {
563                return;
564            }
565            doResize(e);
566            doDoubleSort(e);
567            uncacheResizingColumn();
568        }
569
570        private void doDoubleSort(MouseEvent e) {
571            if (!hasCachedSortColumn() || e.getClickCount() % 2 == 1) return;
572            getXTable().toggleSortOrder(sortColumn);
573            uncacheSortColumn();
574        }
575
576        private boolean hasCachedSortColumn() {
577            return sortColumn >= 0;
578        }
579
580        /**
581         * Resets sort enablement always, set resizing marker if available.
582         */
583        @Override
584        public void mousePressed(MouseEvent e) {
585            resetToggleSortOrder(e);
586            if (shouldIgnore(e)) {
587                return;
588            }
589            cacheResizingColumn(e);
590        }
591
592        /** 
593         * Sets resizing marker if available, disables table sorting if in 
594         * resize region and sort gesture (aka: single click).
595         */
596        @Override
597        public void mouseReleased(MouseEvent e) {
598            if (shouldIgnore(e)) {
599                return;
600            }
601            cacheResizingColumn(e);
602            cacheSortColumn(e);
603            if (isInResizeRegion(e) && e.getClickCount() % 2 == 1) {
604                disableToggleSortOrder(e);
605            } 
606        }
607
608        private void cacheSortColumn(MouseEvent e) {
609            if (!canCacheSortColumn(e)) uncacheSortColumn();
610            if (e.getClickCount() % 2 == 1) {
611                int column = columnAtPoint(e.getPoint());
612                if (column >= 0) {
613                    int primarySortIndex = getXTable().getSortedColumnIndex();
614                    if (primarySortIndex == column) {
615                        column = -1;
616                    }
617                }
618                sortColumn = column;
619            }
620            
621        }
622
623        private void uncacheSortColumn() {
624            sortColumn = -1;
625        }
626
627        private boolean canCacheSortColumn(MouseEvent e) {
628            if (hasSortController() && !isInResizeRegion(e) && getResortsOnDoubleClick()) {
629                return true;
630            }
631            return false;
632        }
633        
634        /**
635         * Returns a boolean indication if the mouse event should be ignored.
636         * Here: returns true if table not enabled or not an event from the left mouse
637         * button.
638         * 
639         * @param e
640         * @return
641         */
642        private boolean shouldIgnore(MouseEvent e) {
643            return !SwingUtilities.isLeftMouseButton(e)
644              || !table.isEnabled();
645        }
646
647        /**
648         * Packs caches resizing column on double click, if available. Does nothing
649         * otherwise.
650         * 
651         * @param e
652         */
653        private void doResize(MouseEvent e) {
654            if (e.getClickCount() != 2)
655                return;
656            int column = getViewIndexForColumn(cachedResizingColumn);
657            if (column >= 0) {
658                (getXTable()).packColumn(column, 5);
659            }
660        }
661
662
663        /**
664         * 
665         * @param e
666         */
667        private void disableToggleSortOrder(MouseEvent e) {
668            if (!hasSortController()) return;
669            SortController<?> controller = (SortController<?>) getXTable().getRowSorter();
670            cachedSortOrderCycle = controller.getSortOrderCycle();
671            controller.setSortOrderCycle();
672        }
673
674        /**
675         * @return
676         */
677        private boolean hasSortController() {
678            return (getXTable().getRowSorter() instanceof SortController<?>);
679        }
680        
681        /**
682         * 
683         */
684        private void resetToggleSortOrder(MouseEvent e) {
685            if (cachedSortOrderCycle == null) return;
686            ((SortController<?>) getXTable().getRowSorter()).setSortOrderCycle(cachedSortOrderCycle);
687            cachedSortOrderCycle = null;
688        }
689
690
691        /**
692         * Caches the resizing column if set. Does nothing if null.
693         *     
694         * @param e
695         */
696        private void cacheResizingColumn(MouseEvent e) {
697            TableColumn column = getResizingColumn();
698            if (column != null) {
699                cachedResizingColumn = column;
700            }
701        }
702
703        /**
704         * Sets the cached resizing column to null.
705         */
706        private void uncacheResizingColumn() {
707            cachedResizingColumn = null;
708        }
709
710        /**
711         * Returns true if the mouseEvent happened in the resizing region.
712         * 
713         * @param e
714         * @return
715         */
716        private boolean isInResizeRegion(MouseEvent e) {
717            return cachedResizingColumn != null; // inResize;
718        }
719
720        @Override
721        public void mouseEntered(MouseEvent e) {
722        }
723
724        /**
725         * Resets all cached state.
726         */
727        @Override
728        public void mouseExited(MouseEvent e) {
729            uncacheSortColumn();
730            uncacheResizingColumn();
731            resetToggleSortOrder(e);
732        }
733
734        /**
735         * Resets all cached state.
736         */
737        @Override
738        public void mouseDragged(MouseEvent e) {
739            uncacheSortColumn();
740            uncacheResizingColumn();
741            resetToggleSortOrder(e);
742        }
743
744        /**
745         * Resets all cached state.
746         */
747        @Override
748        public void mouseMoved(MouseEvent e) {
749            uncacheSortColumn();
750            resetToggleSortOrder(e);
751        }
752    }
753
754    
755
756}