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