001    /*
002     * $Id: JXTable.java,v 1.117 2006/05/14 08:12:18 dmouse 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;
023    
024    import java.awt.Color;
025    import java.awt.Component;
026    import java.awt.ComponentOrientation;
027    import java.awt.Container;
028    import java.awt.Cursor;
029    import java.awt.Dimension;
030    import java.awt.Point;
031    import java.awt.Rectangle;
032    import java.awt.event.ActionEvent;
033    import java.awt.event.ActionListener;
034    import java.awt.print.PrinterException;
035    import java.beans.PropertyChangeEvent;
036    import java.beans.PropertyChangeListener;
037    import java.lang.reflect.Field;
038    import java.text.DateFormat;
039    import java.text.NumberFormat;
040    import java.util.Collections;
041    import java.util.Date;
042    import java.util.Enumeration;
043    import java.util.HashMap;
044    import java.util.Hashtable;
045    import java.util.Iterator;
046    import java.util.List;
047    import java.util.Map;
048    import java.util.Vector;
049    import java.util.logging.Level;
050    import java.util.logging.Logger;
051    import java.util.regex.Matcher;
052    import java.util.regex.Pattern;
053    
054    import javax.swing.AbstractAction;
055    import javax.swing.AbstractButton;
056    import javax.swing.Action;
057    import javax.swing.ActionMap;
058    import javax.swing.DefaultCellEditor;
059    import javax.swing.Icon;
060    import javax.swing.ImageIcon;
061    import javax.swing.JCheckBox;
062    import javax.swing.JComponent;
063    import javax.swing.JLabel;
064    import javax.swing.JScrollPane;
065    import javax.swing.JTable;
066    import javax.swing.JTextField;
067    import javax.swing.JViewport;
068    import javax.swing.KeyStroke;
069    import javax.swing.ListSelectionModel;
070    import javax.swing.ScrollPaneConstants;
071    import javax.swing.SizeSequence;
072    import javax.swing.UIDefaults;
073    import javax.swing.UIManager;
074    import javax.swing.border.Border;
075    import javax.swing.border.EmptyBorder;
076    import javax.swing.border.LineBorder;
077    import javax.swing.event.ChangeEvent;
078    import javax.swing.event.ChangeListener;
079    import javax.swing.event.ListSelectionEvent;
080    import javax.swing.event.TableColumnModelEvent;
081    import javax.swing.event.TableModelEvent;
082    import javax.swing.table.DefaultTableCellRenderer;
083    import javax.swing.table.JTableHeader;
084    import javax.swing.table.TableCellEditor;
085    import javax.swing.table.TableCellRenderer;
086    import javax.swing.table.TableColumn;
087    import javax.swing.table.TableColumnModel;
088    import javax.swing.table.TableModel;
089    
090    import org.jdesktop.swingx.action.BoundAction;
091    import org.jdesktop.swingx.decorator.ComponentAdapter;
092    import org.jdesktop.swingx.decorator.FilterPipeline;
093    import org.jdesktop.swingx.decorator.Highlighter;
094    import org.jdesktop.swingx.decorator.HighlighterPipeline;
095    import org.jdesktop.swingx.decorator.PatternHighlighter;
096    import org.jdesktop.swingx.decorator.PipelineEvent;
097    import org.jdesktop.swingx.decorator.PipelineListener;
098    import org.jdesktop.swingx.decorator.SearchHighlighter;
099    import org.jdesktop.swingx.decorator.SelectionMapper;
100    import org.jdesktop.swingx.decorator.SizeSequenceMapper;
101    import org.jdesktop.swingx.decorator.SortController;
102    import org.jdesktop.swingx.decorator.SortKey;
103    import org.jdesktop.swingx.decorator.SortOrder;
104    import org.jdesktop.swingx.icon.ColumnControlIcon;
105    import org.jdesktop.swingx.plaf.LookAndFeelAddons;
106    import org.jdesktop.swingx.table.ColumnControlButton;
107    import org.jdesktop.swingx.table.ColumnFactory;
108    import org.jdesktop.swingx.table.DefaultTableColumnModelExt;
109    import org.jdesktop.swingx.table.TableColumnExt;
110    import org.jdesktop.swingx.table.TableColumnModelExt;
111    
112    /**
113     * <p>
114     * A JXTable is a JTable with built-in support for row sorting, filtering, and
115     * highlighting, column visibility and a special popup control on the column
116     * header for quick access to table configuration. You can instantiate a JXTable
117     * just as you would a JTable, using a TableModel. However, a JXTable
118     * automatically wraps TableColumns inside a TableColumnExt instance.
119     * TableColumnExt supports visibility, sortability, and prototype values for
120     * column sizing, none of which are available in TableColumn. You can retrieve
121     * the TableColumnExt instance for a column using {@link #getColumnExt(Object)}
122     * or {@link #getColumnExt(int colnumber)}.
123     * 
124     * <p>
125     * A JXTable is, by default, sortable by clicking on column headers; each
126     * subsequent click on a header reverses the order of the sort, and a sort arrow
127     * icon is automatically drawn on the header. Sorting can be disabled using
128     * {@link #setSortable(boolean)}. Sorting on columns is handled by a Sorter
129     * instance which contains a Comparator used to compare values in two rows of a
130     * column. You can replace the Comparator for a given column by using
131     * <code>getColumnExt("column").getSorter().setComparator(customComparator)</code>
132     * 
133     * <p>
134     * Columns can be hidden or shown by setting the visible property on the
135     * TableColumnExt using {@link TableColumnExt#setVisible(boolean)}. Columns can
136     * also be shown or hidden from the column control popup.
137     * 
138     * <p>
139     * The column control popup is triggered by an icon drawn to the far right of
140     * the column headers, above the table's scrollbar (when installed in a
141     * JScrollPane). The popup allows the user to select which columns should be
142     * shown or hidden, as well as to pack columns and turn on horizontal scrolling.
143     * To show or hide the column control, use the
144     * {@link #setColumnControlVisible(boolean show)}method.
145     * 
146     * <p>
147     * Rows can be filtered from a JXTable using a Filter class and a
148     * FilterPipeline. One assigns a FilterPipeline to the table using
149     * {@link #setFilters(FilterPipeline)}. Filtering hides, but does not delete or
150     * permanently remove rows from a JXTable. Filters are used to provide sorting
151     * to the table--rows are not removed, but the table is made to believe rows in
152     * the model are in a sorted order.
153     * 
154     * <p>
155     * One can automatically highlight certain rows in a JXTable by attaching
156     * Highlighters in the {@link #setHighlighters(HighlighterPipeline)}method. An
157     * example would be a Highlighter that colors alternate rows in the table for
158     * readability; AlternateRowHighlighter does this. Again, like Filters,
159     * Highlighters can be chained together in a HighlighterPipeline to achieve more
160     * interesting effects.
161     * 
162     * <p>
163     * You can resize all columns, selected columns, or a single column using the
164     * methods like {@link #packAll()}. Packing combines several other aspects of a
165     * JXTable. If horizontal scrolling is enabled using
166     * {@link #setHorizontalScrollEnabled(boolean)}, then the scrollpane will allow
167     * the table to scroll right-left, and columns will be sized to their preferred
168     * size. To control the preferred sizing of a column, you can provide a
169     * prototype value for the column in the TableColumnExt using
170     * {@link TableColumnExt#setPrototypeValue(Object)}. The prototype is used as
171     * an indicator of the preferred size of the column. This can be useful if some
172     * data in a given column is very long, but where the resize algorithm would
173     * normally not pick this up.
174     * 
175     * <p>
176     * Last, you can also provide searches on a JXTable using the Searchable property.
177     * 
178     * <p>
179     * Keys/Actions registered with this component:
180     * 
181     * <ul>
182     * <li> "find" - open an appropriate search widget for searching cell content. The
183     *   default action registeres itself with the SearchFactory as search target.
184     * <li> "print" - print the table
185     * <li> {@link JXTable#HORIZONTALSCROLL_ACTION_COMMAND} - toggle the horizontal scrollbar
186     * <li> {@link JXTable#PACKSELECTED_ACTION_COMMAND} - resize the selected column to fit the widest
187     *  cell content 
188     * <li> {@link JXTable#PACKALL_ACTION_COMMAND} - resize all columns to fit the widest
189     *  cell content in each column
190     * 
191     * </ul>
192     * 
193     * <p>
194     * Key bindings.
195     * 
196     * <ul>
197     * <li> "control F" - bound to actionKey "find".
198     * </ul>
199     * 
200     * <p>
201     * Client Properties.
202     * 
203     * <ul>
204     * <li> {@link JXTable#MATCH_HIGHLIGHTER} - set to Boolean.TRUE to 
205     *  use a SearchHighlighter to mark a cell as matching.
206     * </ul>
207     * 
208     * @author Ramesh Gupta
209     * @author Amy Fowler
210     * @author Mark Davidson
211     * @author Jeanette Winzenburg
212     */
213    public class JXTable extends JTable { 
214        private static final Logger LOG = Logger.getLogger(JXTable.class.getName());
215        
216    
217        /**
218         * Constant string for horizontal scroll actions, used in JXTable's Action
219         * Map.
220         */
221        public static final String HORIZONTALSCROLL_ACTION_COMMAND = 
222            ColumnControlButton.COLUMN_CONTROL_MARKER + "horizontalScroll";
223    
224        /** Constant string for packing all columns, used in JXTable's Action Map. */
225        public static final String PACKALL_ACTION_COMMAND = 
226            ColumnControlButton.COLUMN_CONTROL_MARKER + "packAll";
227    
228        /**
229         * Constant string for packing selected columns, used in JXTable's Action
230         * Map.
231         */
232        public static final String PACKSELECTED_ACTION_COMMAND = 
233            ColumnControlButton.COLUMN_CONTROL_MARKER + "packSelected";
234    
235        /** The prefix marker to find component related properties in the resourcebundle. */
236        public static final String UIPREFIX = "JXTable.";
237    
238        /** key for client property to use SearchHighlighter as match marker. */
239        public static final String MATCH_HIGHLIGHTER = AbstractSearchable.MATCH_HIGHLIGHTER;
240    
241        static {
242            // Hack: make sure the resource bundle is loaded
243            LookAndFeelAddons.getAddon();
244        }
245    
246        /** The FilterPipeline for the table. */
247        protected FilterPipeline filters;
248    
249        /** The HighlighterPipeline for the table. */
250        protected HighlighterPipeline highlighters;
251    
252        /** The ComponentAdapter for model data access. */
253        protected ComponentAdapter dataAdapter;
254    
255        /** The handler for mapping view/model coordinates of row selection. */
256        private SelectionMapper selectionMapper;
257    
258        /** flag to indicate if table is interactively sortable. */
259        private boolean sortable;
260    
261        /** future - enable/disable autosort on cell updates not used */
262    //    private boolean automaticSortDisabled;
263    
264        /** Listens for changes from the filters. */
265        private PipelineListener pipelineListener;
266    
267        /** Listens for changes from the highlighters. */
268        private ChangeListener highlighterChangeListener;
269    
270        /** the factory to use for column creation and configuration. */
271        private ColumnFactory columnFactory;
272    
273        /** The default number of visible rows (in a ScrollPane). */
274        private int visibleRowCount = 18;
275    
276        private SizeSequenceMapper rowModelMapper;
277    
278        private Field rowModelField;
279    
280        private boolean rowHeightEnabled;
281    
282        /**
283         * flag to indicate if the column control is visible.
284         */
285        private boolean columnControlVisible;
286        /**
287         * ScrollPane's original vertical scroll policy. If the columnControl is
288         * visible the policy is set to ALWAYS.
289         */
290        private int verticalScrollPolicy;
291    
292        /**
293         * A button that allows the user to select which columns to display, and
294         * which to hide
295         */
296        private JComponent columnControlButton;
297    
298        /**
299         * Mouse/Motion/Listener keeping track of mouse moved in cell coordinates.
300         */
301        private RolloverProducer rolloverProducer;
302    
303        /**
304         * RolloverController: listens to cell over events and repaints
305         * entered/exited rows.
306         */
307        private TableRolloverController linkController;
308    
309        /** field to store the autoResizeMode while interactively setting 
310         *  horizontal scrollbar to visible.
311         */
312        private int oldAutoResizeMode;
313    
314        /** temporary hack: rowheight will be internally adjusted to font size 
315         *  on instantiation and in updateUI if 
316         *  the height has not been set explicitly by the application.
317         */
318        protected boolean isXTableRowHeightSet;
319    
320        protected Searchable searchable;
321    
322        private boolean fillsViewportHeight;
323    
324        /** Instantiates a JXTable with a default table model, no data. */
325        public JXTable() {
326            init();
327        }
328    
329        /**
330         * Instantiates a JXTable with a specific table model.
331         * 
332         * @param dm
333         *            The model to use.
334         */
335        public JXTable(TableModel dm) {
336            super(dm);
337            init();
338        }
339    
340        /**
341         * Instantiates a JXTable with a specific table model.
342         * 
343         * @param dm
344         *            The model to use.
345         */
346        public JXTable(TableModel dm, TableColumnModel cm) {
347            super(dm, cm);
348            init();
349        }
350    
351        /**
352         * Instantiates a JXTable with a specific table model, column model, and
353         * selection model.
354         * 
355         * @param dm
356         *            The table model to use.
357         * @param cm
358         *            The colomn model to use.
359         * @param sm
360         *            The list selection model to use.
361         */
362        public JXTable(TableModel dm, TableColumnModel cm, ListSelectionModel sm) {
363            super(dm, cm, sm);
364            init();
365        }
366    
367        /**
368         * Instantiates a JXTable for a given number of columns and rows.
369         * 
370         * @param numRows
371         *            Count of rows to accomodate.
372         * @param numColumns
373         *            Count of columns to accomodate.
374         */
375        public JXTable(int numRows, int numColumns) {
376            super(numRows, numColumns);
377            init();
378        }
379    
380        /**
381         * Instantiates a JXTable with data in a vector or rows and column names.
382         * 
383         * @param rowData
384         *            Row data, as a Vector of Objects.
385         * @param columnNames
386         *            Column names, as a Vector of Strings.
387         */
388        public JXTable(Vector rowData, Vector columnNames) {
389            super(rowData, columnNames);
390            init();
391        }
392    
393        /**
394         * Instantiates a JXTable with data in a array or rows and column names.
395         * 
396         * @param rowData
397         *            Row data, as a two-dimensional Array of Objects (by row, for
398         *            column).
399         * @param columnNames
400         *            Column names, as a Array of Strings.
401         */
402        public JXTable(Object[][] rowData, Object[] columnNames) {
403            super(rowData, columnNames);
404            init();
405        }
406    
407        /** 
408         * Initializes the table for use.
409         *  
410         */
411        /*
412         * PENDING JW: this method should be private!
413         * 
414         */
415        protected void init() {
416            setSortable(true);
417            setRolloverEnabled(true);
418            // guarantee getFilters() to return != null
419            setFilters(null);
420            initActionsAndBindings();
421            // instantiate row height depending on font size
422            updateRowHeightUI(false);
423            setFillsViewportHeight(true);
424        }
425    
426        /**
427         * Property to enable/disable rollover support. This can be enabled to show
428         * "live" rollover behaviour, f.i. the cursor over LinkModel cells. Default
429         * is enabled. If rollover effects are not used, this property should be 
430         * disabled.
431         * 
432         * @param rolloverEnabled
433         */
434        public void setRolloverEnabled(boolean rolloverEnabled) {
435            boolean old = isRolloverEnabled();
436            if (rolloverEnabled == old)
437                return;
438            if (rolloverEnabled) {
439                rolloverProducer = createRolloverProducer();
440                addMouseListener(rolloverProducer);
441                addMouseMotionListener(rolloverProducer);
442                getLinkController().install(this);
443    
444            } else {
445                removeMouseListener(rolloverProducer);
446                removeMouseMotionListener(rolloverProducer);
447                rolloverProducer = null;
448                getLinkController().release();
449            }
450            firePropertyChange("rolloverEnabled", old, isRolloverEnabled());
451        }
452    
453        protected TableRolloverController getLinkController() {
454            if (linkController == null) {
455                linkController = createLinkController();
456            }
457            return linkController;
458        }
459    
460        protected TableRolloverController createLinkController() {
461            return new TableRolloverController();
462        }
463    
464    
465        /**
466         * creates and returns the RolloverProducer to use.
467         * 
468         * @return <code>RolloverProducer</code>
469         */
470        protected RolloverProducer createRolloverProducer() {
471            RolloverProducer r = new RolloverProducer() {
472                protected void updateRolloverPoint(JComponent component,
473                        Point mousePoint) {
474                    JTable table = (JTable) component;
475                    int col = table.columnAtPoint(mousePoint);
476                    int row = table.rowAtPoint(mousePoint);
477                    if ((col < 0) || (row < 0)) {
478                        row = -1;
479                        col = -1;
480                    }
481                    rollover.x = col;
482                    rollover.y = row;
483                }
484    
485            };
486            return r;
487        }
488    
489        /**
490         * Returns the rolloverEnabled property.
491         * 
492         * @return <code>true</code> if rollover is enabled
493         */
494        public boolean isRolloverEnabled() {
495            return rolloverProducer != null;
496        }
497    
498    
499        /**
500         * listens to rollover properties. 
501         * Repaints effected component regions.
502         * Updates link cursor.
503         * 
504         * @author Jeanette Winzenburg
505         */
506        public static class TableRolloverController<T extends JTable>  extends RolloverController<T> {
507    
508            private Cursor oldCursor;
509    
510    //    --------------------------- JTable rollover
511            
512            protected void rollover(Point oldLocation, Point newLocation) {
513                if (oldLocation != null) {
514                    Rectangle r = component.getCellRect(oldLocation.y, oldLocation.x, false);
515                    r.x = 0;
516                    r.width = component.getWidth();
517                    component.repaint(r);
518                }
519                if (newLocation != null) {
520                    Rectangle r = component.getCellRect(newLocation.y, newLocation.x, false);
521                    r.x = 0;
522                    r.width = component.getWidth();
523                    component.repaint(r);
524                }
525                setRolloverCursor(newLocation);
526            }
527    
528            /**
529             * overridden to return false if cell editable.
530             */
531            @Override
532            protected boolean isClickable(Point location) {
533                return super.isClickable(location) && !component.isCellEditable(location.y, location.x);
534            }
535    
536            protected RolloverRenderer getRolloverRenderer(Point location, boolean prepare) {
537                TableCellRenderer renderer = component.getCellRenderer(location.y, location.x);
538                RolloverRenderer rollover = renderer instanceof RolloverRenderer ?
539                        (RolloverRenderer) renderer : null;
540                if ((rollover != null) && !rollover.isEnabled()) {
541                    rollover = null;
542                }
543                if ((rollover != null) && prepare) {
544                    component.prepareRenderer(renderer, location.y, location.x);
545                }
546                return rollover;
547            }
548    
549    
550            private void setRolloverCursor(Point location) {
551                if (hasRollover(location)) {
552                    if (oldCursor == null) {
553                        oldCursor = component.getCursor();
554                        component.setCursor(Cursor
555                                .getPredefinedCursor(Cursor.HAND_CURSOR));
556                    }
557                } else {
558                    if (oldCursor != null) {
559                        component.setCursor(oldCursor);
560                        oldCursor = null;
561                    }
562                }
563    
564            }
565            
566    
567            protected Point getFocusedCell() {
568                int leadRow = component.getSelectionModel()
569                        .getLeadSelectionIndex();
570                int leadColumn = component.getColumnModel().getSelectionModel()
571                        .getLeadSelectionIndex();
572                return new Point(leadColumn, leadRow);
573            }
574    
575        }
576    
577        
578    //--------------------------------- ColumnControl && Viewport
579     
580        /**
581         * Set flag to control JXTable's scrollableTracksViewportHeight 
582         * property.
583         * If true the table's height will be always at least as large as the 
584         * containing (viewport?) parent, if false the table's height will be
585         * independent of parent's height.
586         *   
587         */
588        public void setFillsViewportHeight(boolean fillsViewportHeight) {
589            if (fillsViewportHeight == getFillsViewportHeight()) return;
590            boolean old = getFillsViewportHeight();
591            this.fillsViewportHeight = fillsViewportHeight;
592            firePropertyChange("fillsViewportHeight", old, getFillsViewportHeight());
593            revalidate();
594        }
595        
596        /**
597         * Returns the flag to control JXTable scrollableTracksViewportHeight
598         * property. 
599         * If true the table's height will be always at least as large as the 
600         * containing (viewport?) parent, if false the table's height will be
601         * independent of parent's height.
602         * 
603         * @return true if the table's height will always be at least as large
604         * as the containing parent, false if it is independent
605         */
606        public boolean getFillsViewportHeight() {
607            return fillsViewportHeight;
608    }
609    
610        /**
611         * Overridden to control the tracksHeight property depending on 
612         * fillsViewportHeight and relative size to containing parent (viewport?).
613         * 
614         * @return true if the control flag is true and the containing viewport
615         *          height > prefHeight, else returns false.
616         * 
617         */
618        @Override
619        public boolean getScrollableTracksViewportHeight() {
620            return getFillsViewportHeight()
621            && getParent() instanceof JViewport
622            && (((JViewport)getParent()).getHeight() > getPreferredSize().height);
623        }
624    
625        
626        /**
627         * overridden to addionally configure the upper right corner of an enclosing
628         * scrollpane with the ColumnControl.
629         */
630        @Override
631        protected void configureEnclosingScrollPane() {
632            super.configureEnclosingScrollPane();
633            configureColumnControl();
634    //        configureViewportBackground();
635        }
636    
637        /**
638         * set's the viewports background to this.background.<p> 
639         * 
640         * PENDING: need to
641         * repeat on background changes to this!
642         * @deprecated no longer used - replaced by fillsViewportHeight
643         * 
644         */
645        protected void configureViewportBackground() {
646            Container p = getParent();
647            if (p instanceof JViewport) {
648                p.setBackground(getBackground());
649            }
650        }
651    
652        /**
653         * configure the upper right corner of an enclosing scrollpane with/o the
654         * ColumnControl, depending on setting of columnControl visibility flag.<p>
655         * 
656         * PENDING: should choose corner depending on component orientation.
657         */
658        private void configureColumnControl() {
659            Container p = getParent();
660            if (p instanceof JViewport) {
661                Container gp = p.getParent();
662                if (gp instanceof JScrollPane) {
663                    JScrollPane scrollPane = (JScrollPane) gp;
664                    // Make certain we are the viewPort's view and not, for
665                    // example, the rowHeaderView of the scrollPane -
666                    // an implementor of fixed columns might do this.
667                    JViewport viewport = scrollPane.getViewport();
668                    if (viewport == null || viewport.getView() != this) {
669                        return;
670                    }
671                    if (isColumnControlVisible()) {
672                        verticalScrollPolicy = scrollPane
673                                .getVerticalScrollBarPolicy();
674                        scrollPane.setCorner(JScrollPane.UPPER_TRAILING_CORNER,
675                                getColumnControl());
676    
677                        scrollPane
678                                .setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
679                    } else {
680                        if (verticalScrollPolicy != 0) {
681                            // Fix #155-swingx: reset only if we had force always before
682                            // PENDING: JW - doesn't cope with dynamically changing the policy
683                            // shouldn't be much of a problem because doesn't happen too often?? 
684                            scrollPane.setVerticalScrollBarPolicy(verticalScrollPolicy);
685                        }
686                        try {
687                            scrollPane.setCorner(JScrollPane.UPPER_TRAILING_CORNER,
688                                    null);
689                        } catch (Exception ex) {
690                            // Ignore spurious exception thrown by JScrollPane. This
691                            // is a Swing bug!
692                        }
693    
694                    }
695                }
696            }
697        }
698    
699        /**
700         * Hack around core swing JScrollPane bug: can't cope with
701         * corners when changing component orientation at runtime.
702         * overridden to re-configure the columnControl.
703         */
704        @Override
705        public void setComponentOrientation(ComponentOrientation o) {
706            super.setComponentOrientation(o);
707            configureColumnControl();
708        }
709    
710        /**
711         * returns visibility flag of column control.
712         * <p>
713         * 
714         * Note: if the table is not inside a JScrollPane the column control is not
715         * shown even if this returns true. In this case it's the responsibility of
716         * the client code to actually show it.
717         * 
718         * @return true if the column is visible, false otherwise
719         */
720        public boolean isColumnControlVisible() {
721            return columnControlVisible;
722        }
723    
724        /**
725         * returns the component for column control.
726         * 
727         * @return component for column control
728         */
729        public JComponent getColumnControl() {
730            if (columnControlButton == null) {
731                columnControlButton = new ColumnControlButton(this,
732                        new ColumnControlIcon());
733            }
734            return columnControlButton;
735        }
736    
737        /**
738         * bound property to flag visibility state of column control.
739         * 
740         * @param showColumnControl
741         */
742        public void setColumnControlVisible(boolean showColumnControl) {
743            boolean old = columnControlVisible;
744            this.columnControlVisible = showColumnControl;
745            configureColumnControl();
746            firePropertyChange("columnControlVisible", old, columnControlVisible);
747        }
748    
749        
750    //--------------------- actions
751        
752        /**
753         * A small class which dispatches actions. TODO: Is there a way that we can
754         * make this static? JW: I hate those if constructs... we are in OO-land!
755         */
756        private class Actions extends UIAction {
757            Actions(String name) {
758                super(name);
759            }
760    
761            public void actionPerformed(ActionEvent evt) {
762                if ("print".equals(getName())) {
763                    try {
764                        print();
765                    } catch (PrinterException ex) {
766                        // REMIND(aim): should invoke pluggable application error
767                        // handler
768                        LOG.log(Level.WARNING, "", ex);
769                    }
770                } else if ("find".equals(getName())) {
771                    find();
772                }
773            }
774    
775        }
776    
777    
778        private void initActionsAndBindings() {
779            // Register the actions that this class can handle.
780            ActionMap map = getActionMap();
781            map.put("print", new Actions("print"));
782            map.put("find", new Actions("find"));
783            map.put(PACKALL_ACTION_COMMAND, createPackAllAction());
784            map.put(PACKSELECTED_ACTION_COMMAND, createPackSelectedAction());
785            map.put(HORIZONTALSCROLL_ACTION_COMMAND, createHorizontalScrollAction());
786            // JW: this should be handled by the LF!
787            KeyStroke findStroke = KeyStroke.getKeyStroke("control F");
788            getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(findStroke, "find");
789        }
790    
791        /** Creates an Action for horizontal scrolling. */
792        private Action createHorizontalScrollAction() {
793            String actionName = getUIString(HORIZONTALSCROLL_ACTION_COMMAND);
794            BoundAction action = new BoundAction(actionName,
795                    HORIZONTALSCROLL_ACTION_COMMAND);
796            action.setStateAction();
797            action.registerCallback(this, "setHorizontalScrollEnabled");
798            action.setSelected(isHorizontalScrollEnabled());
799            return action;
800        }
801    
802        private String getUIString(String key) {
803            String text = UIManager.getString(UIPREFIX + key);
804            return text != null ? text : key;
805        }
806    
807        /** Creates an Action for packing selected columns. */
808        private Action createPackSelectedAction() {
809            String text = getUIString(PACKSELECTED_ACTION_COMMAND);
810            BoundAction action = new BoundAction(text, PACKSELECTED_ACTION_COMMAND);
811            action.registerCallback(this, "packSelected");
812            action.setEnabled(getSelectedColumnCount() > 0);
813            return action;
814        }
815    
816        /** Creates an Action for packing all columns. */
817        private Action createPackAllAction() {
818            String text = getUIString(PACKALL_ACTION_COMMAND);
819            BoundAction action = new BoundAction(text, PACKALL_ACTION_COMMAND);
820            action.registerCallback(this, "packAll");
821            return action;
822        }
823    
824        
825    //------------------ bound action callback methods
826        
827        /**
828         * This resizes all columns to fit the viewport; if horizontal scrolling is
829         * enabled, all columns will get their preferred width. This can be
830         * triggered by the "packAll" BoundAction on the table as well.
831         */
832        public void packAll() {
833            packTable(getDefaultPackMargin());
834        }
835    
836        /**
837         * This resizes selected columns to fit the viewport; if horizontal
838         * scrolling is enabled, selected columns will get their preferred width.
839         * This can be triggered by the "packSelected" BoundAction on the table as
840         * well.
841         */
842        public void packSelected() {
843            int selected = getColumnModel().getSelectionModel().getLeadSelectionIndex();
844            if (selected >= 0) {
845                packColumn(selected, getDefaultPackMargin());
846            }
847        }
848    
849        /**
850         * Controls horizontal scrolling in the viewport, and works in coordination
851         * with column sizing.
852         * 
853         * @param enabled
854         *            If true, the scrollpane will allow the table to scroll
855         *            horizontally, and columns will resize to their preferred
856         *            width. If false, columns will resize to fit the viewport.
857         */
858        public void setHorizontalScrollEnabled(boolean enabled) {
859            if (enabled == (isHorizontalScrollEnabled()))
860                return;
861            if (enabled) {
862                oldAutoResizeMode = getAutoResizeMode();
863                setAutoResizeMode(AUTO_RESIZE_OFF);
864            } else {
865                setAutoResizeMode(oldAutoResizeMode);
866            }
867        }
868    
869        /** Returns the current setting for horizontal scrolling. */
870        private boolean isHorizontalScrollEnabled() {
871            return getAutoResizeMode() == AUTO_RESIZE_OFF;
872        }
873    
874        /** Returns the default margin for packing columns. */
875        private int getDefaultPackMargin() {
876            return 4;
877        }
878    
879        /** Notifies the table that a new column has been selected. 
880         *  overridden to update the enabled state of the packSelected
881         *  action.
882         */
883        @Override
884        public void columnSelectionChanged(ListSelectionEvent e) {
885            super.columnSelectionChanged(e);
886            if (e.getValueIsAdjusting())
887                return;
888            Action packSelected = getActionMap().get(PACKSELECTED_ACTION_COMMAND);
889            if ((packSelected != null)) {
890                packSelected.setEnabled(!((ListSelectionModel) e.getSource())
891                        .isSelectionEmpty());
892            }
893        }
894    
895        /** 
896         * overridden to update the show horizontal scrollbar action's
897         * selected state. 
898         */
899        @Override
900        public void setAutoResizeMode(int mode) {
901            super.setAutoResizeMode(mode);
902            Action showHorizontal = getActionMap().get(
903                    HORIZONTALSCROLL_ACTION_COMMAND);
904            if (showHorizontal instanceof BoundAction) {
905                ((BoundAction) showHorizontal)
906                        .setSelected(isHorizontalScrollEnabled());
907            }
908        }
909    
910    
911    //------------------------ override super because of filter-awareness
912        
913        /**
914         * Returns the row count in the table; if filters are applied, this is the
915         * filtered row count.
916         */
917        @Override
918        public int getRowCount() {
919            // RG: If there are no filters, call superclass version rather than
920            // accessing model directly
921            return filters == null ?
922                    super.getRowCount() : filters.getOutputSize();
923        }
924    
925        public boolean isHierarchical(int column) {
926            return false;
927        }
928    
929        /**
930         * Convert row index from view coordinates to model coordinates accounting
931         * for the presence of sorters and filters.
932         * 
933         * @param row
934         *            row index in view coordinates
935         * @return row index in model coordinates
936         */
937        public int convertRowIndexToModel(int row) {
938            return getFilters().convertRowIndexToModel(row);
939        }
940    
941        /**
942         * Convert row index from model coordinates to view coordinates accounting
943         * for the presence of sorters and filters.
944         * 
945         * @param row
946         *            row index in model coordinates
947         * @return row index in view coordinates
948         */
949        public int convertRowIndexToView(int row) {
950            return getFilters().convertRowIndexToView(row);
951        }
952    
953        /**
954         * {@inheritDoc}
955         */
956        @Override
957        public Object getValueAt(int row, int column) {
958            return getModel().getValueAt(convertRowIndexToModel(row), 
959                    convertColumnIndexToModel(column));
960        }
961    
962        /**
963         * {@inheritDoc}
964         */
965        @Override
966        public void setValueAt(Object aValue, int row, int column) {
967            getModel().setValueAt(aValue, convertRowIndexToModel(row),
968                    convertColumnIndexToModel(column));
969        }
970    
971        /**
972         * Overridden to account for row index mapping and to respect
973         * {@link TableColumnExt#isEditable()} property.
974         * {@inheritDoc}
975         */
976        @Override
977        public boolean isCellEditable(int row, int column) {
978            boolean editable = getModel().isCellEditable(convertRowIndexToModel(row),
979                    convertColumnIndexToModel(column));
980            if (editable) {
981                TableColumnExt tableColumn = getColumnExt(column);
982                if (tableColumn != null) {
983                    editable = editable && tableColumn.isEditable();
984                }
985            }
986            return editable;
987        }
988    
989        
990        /**
991         * Overridden to update selectionMapper
992         */
993        @Override 
994        public void setSelectionModel(ListSelectionModel newModel) {
995            super.setSelectionModel(newModel);
996            getSelectionMapper().setViewSelectionModel(getSelectionModel());
997        }
998    
999        /**
1000         * {@inheritDoc}
1001         */
1002        @Override
1003        public void setModel(TableModel newModel) {
1004            // JW: need to look here? is done in tableChanged as well. 
1005            getSelectionMapper().lock();
1006            super.setModel(newModel);
1007        }
1008    
1009        /** 
1010         * additionally updates filtered state.
1011         * {@inheritDoc}
1012         */
1013        @Override
1014        public void tableChanged(TableModelEvent e) {
1015            if (getSelectionModel().getValueIsAdjusting()) {
1016                // this may happen if the uidelegate/editor changed selection
1017                // and adjusting state
1018                // before firing a editingStopped
1019                // need to enforce update of model selection
1020                getSelectionModel().setValueIsAdjusting(false);
1021            }
1022            // JW: make SelectionMapper deaf ... super doesn't know about row
1023            // mapping and sets rowSelection in model coordinates
1024            // causing complete confusion.
1025            getSelectionMapper().lock();
1026            super.tableChanged(e);
1027            updateSelectionAndRowModel(e);
1028            use(filters);
1029        }
1030    
1031        
1032        /**
1033         * reset model selection coordinates in SelectionMapper after
1034         * model events.
1035         * 
1036         * @param e
1037         */
1038        private void updateSelectionAndRowModel(TableModelEvent e) {
1039            // JW: c&p from JTable
1040            if (e.getType() == TableModelEvent.INSERT) {
1041                int start = e.getFirstRow();
1042                int end = e.getLastRow();
1043                if (start < 0) {
1044                    start = 0;
1045                }
1046                if (end < 0) {
1047                    end = getModel().getRowCount() - 1;
1048                }
1049    
1050                // Adjust the selectionMapper to account for the new rows.
1051                int length = end - start + 1;
1052                getSelectionMapper().insertIndexInterval(start, length, true);
1053                getRowModelMapper().insertIndexInterval(start, length, getRowHeight());
1054    
1055            } else if (e.getType() == TableModelEvent.DELETE) {
1056                int start = e.getFirstRow();
1057                int end = e.getLastRow();
1058                if (start < 0) {
1059                    start = 0;
1060                }
1061                if (end < 0) {
1062                    end = getModel().getRowCount() - 1;
1063                }
1064    
1065                int deletedCount = end - start + 1;
1066                // Adjust the selectionMapper to account for the new rows
1067                getSelectionMapper().removeIndexInterval(start, end);
1068                getRowModelMapper().removeIndexInterval(start, deletedCount);
1069    
1070            } else if (isDataChanged(e) || isStructureChanged(e)) {
1071            
1072                // JW fixing part of #172 - trying to adjust lead/anchor to valid
1073                // indices (at least in model coordinates) after super's default clearSelection
1074                // in dataChanged/structureChanged. 
1075                hackLeadAnchor(e);
1076    
1077                getSelectionMapper().clearModelSelection();
1078                getRowModelMapper().clearModelSizes();
1079                updateViewSizeSequence();
1080                 
1081            } 
1082            // nothing to do on TableEvent.updated
1083    
1084        }
1085    
1086        private boolean isDataChanged(TableModelEvent e) {
1087            return e.getType() == TableModelEvent.UPDATE && 
1088                e.getFirstRow() == 0 &&
1089                e.getLastRow() == Integer.MAX_VALUE;
1090        }
1091    
1092        private boolean isStructureChanged(TableModelEvent e) {
1093            return e == null || e.getFirstRow() == TableModelEvent.HEADER_ROW;
1094        }
1095    
1096    
1097        /**
1098         * Trying to hack around #172-swingx: lead/anchor of row selection model
1099         * is not adjusted to valid (not even model indices!) in the 
1100         * usual clearSelection after dataChanged/structureChanged.
1101         * 
1102         * Note: as of jdk1.5U6 the anchor/lead of the view selectionModel is 
1103         * unconditionally set to -1 after data/structureChanged.
1104         * 
1105         * @param e
1106         */
1107        private void hackLeadAnchor(TableModelEvent e) {
1108            int lead = getSelectionModel().getLeadSelectionIndex();
1109            int anchor = getSelectionModel().getAnchorSelectionIndex();
1110            int lastRow = getModel().getRowCount() - 1;
1111            if ((lead > lastRow) || (anchor > lastRow)) {
1112                lead = anchor = lastRow;
1113                getSelectionModel().setAnchorSelectionIndex(lead);
1114                getSelectionModel().setLeadSelectionIndex(lead);
1115            }
1116        }
1117    
1118        /**
1119         * Called if individual row height mapping need to be updated.
1120         * This implementation guards against unnessary access of 
1121         * super's private rowModel field.
1122         */
1123        protected void updateViewSizeSequence() {
1124            SizeSequence sizeSequence = null;
1125            if (isRowHeightEnabled()) {
1126                sizeSequence = getSuperRowModel();
1127            }
1128            getRowModelMapper().setViewSizeSequence(sizeSequence, getRowHeight());
1129        }
1130        
1131        /**
1132         * temporaryly exposed for testing...
1133         * @return <code>SelectionMapper</code>
1134         */
1135        protected SelectionMapper getSelectionMapper() {
1136            if (selectionMapper == null) {
1137                selectionMapper = new SelectionMapper(filters, getSelectionModel());
1138            }
1139            return selectionMapper;
1140        }
1141    
1142    
1143    //----------------------------- filters
1144        
1145        /** Returns the FilterPipeline for the table. */
1146        public FilterPipeline getFilters() {
1147            // PENDING: this is guaranteed to be != null because
1148            // init calls setFilters(null) which enforces an empty
1149            // pipeline
1150            return filters;
1151        }
1152    
1153        /**
1154         * setModel() and setFilters() may be called in either order.
1155         * 
1156         * @param pipeline
1157         */
1158        private void use(FilterPipeline pipeline) {
1159            if (pipeline != null) {
1160                // check JW: adding listener multiple times (after setModel)?
1161                if (initialUse(pipeline)) {
1162                    pipeline.addPipelineListener(getFilterPipelineListener());
1163                    pipeline.assign(getComponentAdapter());
1164                } else {
1165                    pipeline.flush();
1166                }
1167            }
1168        }
1169    
1170        /**
1171         * @return true is not yet used in this JXTable, false otherwise
1172         */
1173        private boolean initialUse(FilterPipeline pipeline) {
1174            if (pipelineListener == null) return true;
1175            PipelineListener[] l = pipeline.getPipelineListeners();
1176            for (int i = 0; i < l.length; i++) {
1177                if (pipelineListener.equals(l[i]))
1178                    return false;
1179            }
1180            return true;
1181        }
1182    
1183        /** Sets the FilterPipeline for filtering table rows. */
1184        public void setFilters(FilterPipeline pipeline) {
1185            FilterPipeline old = getFilters();
1186            List<? extends SortKey> sortKeys = null;
1187            if (old != null) {
1188                old.removePipelineListener(pipelineListener);
1189                sortKeys = old.getSortController().getSortKeys();
1190            }
1191            if (pipeline == null) {
1192                pipeline = new FilterPipeline();
1193            }
1194            filters = pipeline;
1195            filters.getSortController().setSortKeys(sortKeys);
1196            // JW: first assign to prevent (short?) illegal internal state
1197            // #173-swingx
1198            use(filters);
1199            getRowModelMapper().setFilters(filters);
1200            getSelectionMapper().setFilters(filters);
1201        }
1202    
1203    
1204        /** returns the listener for changes in filters. */
1205        protected PipelineListener getFilterPipelineListener() {
1206            if (pipelineListener == null) {
1207                pipelineListener = createPipelineListener();
1208            }
1209            return pipelineListener;
1210        }
1211    
1212        /** creates the listener for changes in filters. */
1213        protected PipelineListener createPipelineListener() {
1214            PipelineListener l = new PipelineListener() {
1215                public void contentsChanged(PipelineEvent e) {
1216                    updateOnFilterContentChanged();
1217                }
1218            };
1219            return l;
1220        }
1221    
1222    
1223        /** 
1224         * method called on change notification from filterpipeline.
1225         */
1226        protected void updateOnFilterContentChanged() {
1227            revalidate();
1228            repaint();
1229        }
1230    
1231    
1232    //-------------------------------- sorting 
1233    
1234        /**
1235         * Sets &quot;sortable&quot; property indicating whether or not this table
1236         * supports sortable columns. If <code>sortable</code> is
1237         * <code>true</code> then sorting will be enabled on all columns whose
1238         * <code>sortable</code> property is <code>true</code>. If
1239         * <code>sortable</code> is <code>false</code> then sorting will be
1240         * disabled for all columns, regardless of each column's individual
1241         * <code>sorting</code> property. The default is <code>true</code>.
1242         * 
1243         * @see TableColumnExt#isSortable()
1244         * @param sortable
1245         *            boolean indicating whether or not this table supports sortable
1246         *            columns
1247         */
1248        public void setSortable(boolean sortable) {
1249            if (sortable == isSortable())
1250                return;
1251            this.sortable = sortable;
1252            if (!isSortable()) resetSortOrder();
1253            firePropertyChange("sortable", !sortable, sortable);
1254        }
1255    
1256        /** Returns true if the table is sortable. */
1257        public boolean isSortable() {
1258            return sortable;
1259        }
1260    
1261    
1262        /**
1263         * Removes the interactive sorter.
1264         * Used by headerListener.
1265         * 
1266         */
1267        public void resetSortOrder() {
1268            // JW PENDING: think about notification instead of manual repaint.
1269            SortController controller = getSortController();
1270            if (controller != null) {
1271                controller.setSortKeys(null);
1272            }
1273            if (getTableHeader() != null) {
1274                getTableHeader().repaint();
1275            }
1276        }
1277    
1278        /**
1279         * 
1280         * request to sort the column at columnIndex. If there
1281         * is already an interactive sorter for this column it's sort order is
1282         * reversed. Otherwise the columns sorter is used as is.
1283         * Used by headerListener.
1284         * PRE: 0 <= columnIndex < getColumnCount() 
1285         * @param columnIndex the columnIndex in view coordinates.
1286         * 
1287         */
1288        public void toggleSortOrder(int columnIndex) {
1289            if (!isSortable())
1290                return;
1291            SortController controller = getSortController();
1292            if (controller != null) {
1293                TableColumnExt columnExt = getColumnExt(columnIndex);
1294                controller.toggleSortOrder(convertColumnIndexToModel(columnIndex),
1295                        columnExt != null ? columnExt.getComparator() : null);
1296            }
1297        }
1298    
1299    
1300        /**
1301         * Returns the SortOrder of the interactive sorter 
1302         * if it is set from the given column.
1303         * Used by ColumnHeaderRenderer.getTableCellRendererComponent().
1304         * 
1305         * @param columnIndex the column index in view coordinates.
1306         * @return the interactive sorter's SortOrder if matches the column 
1307         *  or SortOrder.UNCHANGED 
1308         */
1309        public SortOrder getSortOrder(int columnIndex) {
1310            SortController sortController = getSortController();
1311            if (sortController == null) return SortOrder.UNSORTED;
1312            SortKey sortKey = SortKey.getFirstSortKeyForColumn(sortController.getSortKeys(), 
1313                    convertColumnIndexToModel(columnIndex));
1314            return sortKey != null ? sortKey.getSortOrder() : SortOrder.UNSORTED;
1315        }
1316    
1317    
1318        /**
1319         * returns the currently active SortController. Can be null
1320         * on the very first call after instantiation.
1321         * @return the currently active <code>SortController</code> may be null
1322         */
1323        protected SortController getSortController() {
1324    //      // this check is for the sake of the very first call after instantiation
1325            if (filters == null) return null;
1326            return getFilters().getSortController();
1327        }
1328    
1329        /**
1330         * 
1331         * @return the currently interactively sorted TableColumn or null
1332         *   if there is not sorter active or if the sorted column index 
1333         *   does not correspond to any column in the TableColumnModel.
1334         */
1335        public TableColumn getSortedColumn() {
1336            // bloody hack: get sorter and check if there's a column with it
1337            // available
1338            SortController controller = getSortController();
1339            if (controller != null) {
1340                SortKey sortKey = SortKey.getFirstSortingKey(controller.getSortKeys());
1341                if (sortKey != null) {
1342                  int sorterColumn = sortKey.getColumn();
1343                  List columns = getColumns(true);
1344                  for (Iterator iter = columns.iterator(); iter.hasNext();) {
1345                      TableColumn column = (TableColumn) iter.next();
1346                      if (column.getModelIndex() == sorterColumn) {
1347                          return column;
1348                      }
1349                  }
1350                    
1351                }
1352            }
1353            return null;
1354        }
1355    
1356    
1357    
1358        /**
1359         * overridden to remove the interactive sorter if the
1360         * sorted column is no longer contained in the ColumnModel.
1361         */
1362        @Override
1363        public void columnRemoved(TableColumnModelEvent e) {
1364            // JW - old problem: need access to removed column
1365            // to get hold of removed modelIndex
1366            // to remove interactive sorter if any
1367            // no way
1368            // int modelIndex = convertColumnIndexToModel(e.getFromIndex());
1369            updateSorterAfterColumnRemoved();
1370            super.columnRemoved(e);
1371        }
1372    
1373        /**
1374         * guarantee that the interactive sorter is removed if its column
1375         * is removed.
1376         * 
1377         */
1378        private void updateSorterAfterColumnRemoved() {
1379            TableColumn sortedColumn = getSortedColumn();
1380            if (sortedColumn == null) {
1381                resetSortOrder();
1382            }
1383        }
1384    
1385    //---------------------- enhanced TableColumn/Model support    
1386        /**
1387         * Remove all columns, make sure to include hidden.
1388         * 
1389         */
1390        protected void removeColumns() {
1391            /**
1392             * TODO: promote this method to superclass, and change
1393             *       createDefaultColumnsFromModel() to call this method
1394             */
1395            List columns = getColumns(true);
1396            for (Iterator iter = columns.iterator(); iter.hasNext();) {
1397                getColumnModel().removeColumn((TableColumn) iter.next());
1398    
1399            }
1400        }
1401    
1402        /**
1403         * returns a list of all visible TableColumns.
1404         * 
1405         * @return list of all the visible <code>TableColumns</code>
1406         */
1407        public List getColumns() {
1408            return Collections.list(getColumnModel().getColumns());
1409        }
1410    
1411        /**
1412         * returns a list of TableColumns including hidden if the parameter is set
1413         * to true.
1414         * 
1415         * @param includeHidden
1416         * @return list of <code>TableColumns</code> including hidden columns if
1417         * specified
1418         */
1419        public List getColumns(boolean includeHidden) {
1420            if (includeHidden && (getColumnModel() instanceof TableColumnModelExt)) {
1421                return ((TableColumnModelExt) getColumnModel())
1422                        .getColumns(includeHidden);
1423            }
1424            return getColumns();
1425        }
1426    
1427        /**
1428         * returns the number of TableColumns including hidden if the parameter is set 
1429         * to true.
1430         * 
1431         * @param includeHidden
1432         * @return number of <code>TableColumns</code> including hidden columns
1433         * if specified
1434         */
1435        public int getColumnCount(boolean includeHidden) {
1436            if (getColumnModel() instanceof TableColumnModelExt) {
1437                return ((TableColumnModelExt) getColumnModel())
1438                        .getColumnCount(includeHidden);
1439            }
1440            return getColumnCount();
1441        }
1442    
1443        /**
1444         * reorders the columns in the sequence given array. Logical names that do
1445         * not correspond to any column in the model will be ignored. Columns with
1446         * logical names not contained are added at the end.
1447         * 
1448         * @param identifiers
1449         *            array of logical column names
1450         */
1451        public void setColumnSequence(Object[] identifiers) {
1452            List columns = getColumns(true);
1453            Map map = new HashMap();
1454            for (Iterator iter = columns.iterator(); iter.hasNext();) {
1455                // PENDING: handle duplicate identifiers ...
1456                TableColumn column = (TableColumn) iter.next();
1457                map.put(column.getIdentifier(), column);
1458                getColumnModel().removeColumn(column);
1459            }
1460            for (int i = 0; i < identifiers.length; i++) {
1461                TableColumn column = (TableColumn) map.get(identifiers[i]);
1462                if (column != null) {
1463                    getColumnModel().addColumn(column);
1464                    columns.remove(column);
1465                }
1466            }
1467            for (Iterator iter = columns.iterator(); iter.hasNext();) {
1468                TableColumn column = (TableColumn) iter.next();
1469                getColumnModel().addColumn(column);
1470            }
1471        }
1472    
1473        /**
1474         * Returns the <code>TableColumnExt</code> object for the column in the
1475         * table whose identifier is equal to <code>identifier</code>, when
1476         * compared using <code>equals</code>. The returned TableColumn is
1477         * guaranteed to be part of the current ColumnModel but may be hidden, that
1478         * is
1479         * 
1480         * <pre> <code>
1481         * TableColumnExt column = table.getColumnExt(id);
1482         * if (column != null) {
1483         *     int viewIndex = table.convertColumnIndexToView(column.getModelIndex());
1484         *     assertEquals(column.isVisible(), viewIndex &gt;= 0);
1485         * }
1486         * </code> </pre>
1487         * 
1488         * @param identifier
1489         *            the identifier object
1490         * 
1491         * @return the <code>TableColumnExt</code> object that matches the
1492         *         identifier or null if none is found.
1493         */
1494        public TableColumnExt getColumnExt(Object identifier) {
1495            if (getColumnModel() instanceof TableColumnModelExt) {
1496                return ((TableColumnModelExt) getColumnModel())
1497                        .getColumnExt(identifier);
1498            } else {
1499                // PENDING: not tested!
1500                try {
1501                    TableColumn column = getColumn(identifier);
1502                    if (column instanceof TableColumnExt) {
1503                        return (TableColumnExt) column;
1504                    }
1505                } catch (Exception e) {
1506                    // TODO: handle exception
1507                }
1508            }
1509            return null;
1510        }
1511    
1512        /**
1513         * Returns the <code>TableColumnExt</code> object for the column in the
1514         * table whose column index is equal to <code>viewColumnIndex</code> or 
1515         * null if the column is not of type <code>TableColumnExt</code>
1516         * 
1517         * @param viewColumnIndex
1518         *            index of the column with the object in question
1519         * 
1520         * @return the <code>TableColumnExt</code> object that matches the column
1521         *         index
1522         * @throws ArrayIndexOutOfBoundsException if viewColumnIndex out of allowed range.
1523         */
1524        public TableColumnExt getColumnExt(int viewColumnIndex) {
1525            TableColumn column = getColumn(viewColumnIndex);
1526            if (column instanceof TableColumnExt) {
1527                return (TableColumnExt) column;
1528            }
1529            return null;
1530        }
1531    
1532        /**
1533         * Returns the <code>TableColumn</code> object for the column in the
1534         * table whose column index is equal to <code>viewColumnIndex</code>.
1535         * 
1536         * Note: 
1537         * Super does not expose the TableColumn access by index which may lead to
1538         * unexpected IllegalArgumentException if client code assumes the delegate
1539         * method is available - autoboxing will convert the given int to an object
1540         * which will call the getColumn(Object) method ... We do here.
1541         * 
1542         * 
1543         * @param viewColumnIndex
1544         *            index of the column with the object in question
1545         * 
1546         * @return the <code>TableColumn</code> object that matches the column
1547         *         index
1548         * @throws ArrayIndexOutOfBoundsException if viewColumnIndex out of allowed range.
1549         */
1550        public TableColumn getColumn(int viewColumnIndex) {
1551            return getColumnModel().getColumn(viewColumnIndex);
1552        }
1553    
1554        @Override
1555        public void createDefaultColumnsFromModel() {
1556            TableModel model = getModel();
1557            if (model != null) {
1558                // Create new columns from the data model info
1559                // Note: it's critical to create the new columns before
1560                // deleting the old ones. Why?
1561                // JW PENDING: the reason is somewhere in the early forums - search!
1562                int modelColumnCount = model.getColumnCount();
1563                TableColumn newColumns[] = new TableColumn[modelColumnCount];
1564                for (int i = 0; i < newColumns.length; i++) {
1565                    newColumns[i] = createAndConfigureColumn(model, i);
1566                }
1567                // Remove any current columns
1568                removeColumns();
1569                // Now add the new columns to the column model
1570                for (int i = 0; i < newColumns.length; i++) {
1571                    addColumn(newColumns[i]);
1572                }
1573            }
1574        }
1575    
1576    
1577        protected TableColumn createAndConfigureColumn(TableModel model,
1578                int modelColumn) {
1579            return getColumnFactory().createAndConfigureTableColumn(model,
1580                    modelColumn);
1581        }
1582    
1583        protected ColumnFactory getColumnFactory() {
1584            if (columnFactory == null) {
1585                columnFactory = ColumnFactory.getInstance();
1586            }
1587            return columnFactory;
1588        }
1589    
1590    
1591    
1592        
1593    //----------------------- delegating methods?? from super    
1594        /**
1595         * Returns the margin between columns.
1596         * 
1597         * @return the margin between columns
1598         */
1599        public int getColumnMargin() {
1600            return getColumnModel().getColumnMargin();
1601        }
1602    
1603        /**
1604         * Sets the margin between columns.
1605         * 
1606         * @param value
1607         *            margin between columns; must be greater than or equal to zero.
1608         */
1609        public void setColumnMargin(int value) {
1610            getColumnModel().setColumnMargin(value);
1611        }
1612    
1613        /**
1614         * Returns the selection mode used by this table's selection model.
1615         * 
1616         * @return the selection mode used by this table's selection model
1617         */
1618        public int getSelectionMode() {
1619            return getSelectionModel().getSelectionMode();
1620        }
1621    
1622    //----------------------- Search support 
1623    
1624    
1625        /** Opens the find widget for the table. */
1626        private void find() {
1627            SearchFactory.getInstance().showFindInput(this, getSearchable());
1628        }
1629    
1630        /**
1631         * 
1632         * @return a not-null Searchable for this editor.
1633         */
1634        public Searchable getSearchable() {
1635            if (searchable == null) {
1636                searchable = new TableSearchable();
1637            }
1638            return searchable;
1639        }
1640    
1641        /**
1642         * sets the Searchable for this editor. If null, a default 
1643         * searchable will be used.
1644         * 
1645         * @param searchable
1646         */
1647        public void setSearchable(Searchable searchable) {
1648            this.searchable = searchable;
1649        }
1650    
1651        public class TableSearchable extends AbstractSearchable {
1652    
1653            private SearchHighlighter searchHighlighter;
1654            
1655    
1656            protected void findMatchAndUpdateState(Pattern pattern, int startRow,
1657                    boolean backwards) {
1658                SearchResult matchRow = null;
1659                if (backwards) {
1660                    // CHECK: off-one end still needed?
1661                    // Probably not - the findXX don't have side-effects any longer
1662                    // hmmm... still needed: even without side-effects we need to
1663                    // guarantee calling the notfound update at the very end of the
1664                    // loop.
1665                    for (int r = startRow; r >= -1 && matchRow == null; r--) {
1666                        matchRow = findMatchBackwardsInRow(pattern, r);
1667                        updateState(matchRow);
1668                    }
1669                } else {
1670                    for (int r = startRow; r <= getSize() && matchRow == null; r++) {
1671                        matchRow = findMatchForwardInRow(pattern, r);
1672                        updateState(matchRow);
1673                    }
1674                }
1675                // KEEP - JW: Needed to update if loop wasn't entered!
1676                // the alternative is to go one off in the loop. Hmm - which is
1677                // preferable?
1678                // updateState(matchRow);
1679    
1680            }
1681    
1682            /**
1683             * called if sameRowIndex && !hasEqualRegEx. Matches the cell at
1684             * row/lastFoundColumn against the pattern. PRE: lastFoundColumn valid.
1685             * 
1686             * @param pattern
1687             * @param row
1688             * @return an appropriate <code>SearchResult</code> if matching or null
1689             */
1690            protected SearchResult findExtendedMatch(Pattern pattern, int row) {
1691                return findMatchAt(pattern, row, lastSearchResult.foundColumn);
1692            }
1693    
1694            /**
1695             * Searches forward through columns of the given row. Starts at
1696             * lastFoundColumn or first column if lastFoundColumn < 0. returns an
1697             * appropriate SearchResult if a matching cell is found in this row or
1698             * null if no match is found. A row index out off range results in a
1699             * no-match.
1700             * 
1701             * @param pattern
1702             * @param row
1703             *            the row to search
1704             * @return an appropriate <code>SearchResult</code> if a matching cell
1705             * is found in this row or null if no match is found
1706             */
1707            private SearchResult findMatchForwardInRow(Pattern pattern, int row) {
1708                int startColumn = (lastSearchResult.foundColumn < 0) ? 0 : lastSearchResult.foundColumn;
1709                if (isValidIndex(row)) {
1710                    for (int column = startColumn; column < getColumnCount(); column++) {
1711                        SearchResult result = findMatchAt(pattern, row, column);
1712                        if (result != null)
1713                            return result;
1714                    }
1715                }
1716                return null;
1717            }
1718    
1719            /**
1720             * Searches forward through columns of the given row. Starts at
1721             * lastFoundColumn or first column if lastFoundColumn < 0. returns an
1722             * appropriate SearchResult if a matching cell is found in this row or
1723             * null if no match is found. A row index out off range results in a
1724             * no-match.
1725             * 
1726             * @param pattern
1727             * @param row
1728             *            the row to search
1729             * @return an appropriate <code>SearchResult</code> if a matching cell is found
1730             * in this row or null if no match is found
1731             */
1732            private SearchResult findMatchBackwardsInRow(Pattern pattern, int row) {
1733                int startColumn = (lastSearchResult.foundColumn < 0) ? getColumnCount() - 1
1734                        : lastSearchResult.foundColumn;
1735                if (isValidIndex(row)) {
1736                    for (int column = startColumn; column >= 0; column--) {
1737                        SearchResult result = findMatchAt(pattern, row, column);
1738                        if (result != null)
1739                            return result;
1740                    }
1741                }
1742                return null;
1743            }
1744    
1745            /**
1746             * Matches the cell content at row/col against the given Pattern.
1747             * Returns an appropriate SearchResult if matching or null if no
1748             * matching
1749             * 
1750             * @param pattern
1751             * @param row
1752             *            a valid row index in view coordinates
1753             * @param column
1754             *            a valid column index in view coordinates
1755             * @return an appropriate <code>SearchResult</code> if matching or null
1756             */
1757            protected SearchResult findMatchAt(Pattern pattern, int row, int column) {
1758                Object value = getValueAt(row, column);
1759                if (value != null) {
1760                    Matcher matcher = pattern.matcher(value.toString());
1761                    if (matcher.find()) {
1762                        return createSearchResult(matcher, row, column);
1763                    }
1764                }
1765                return null;
1766            }
1767    
1768            /**
1769             * Called if startIndex is different from last search, reset the column
1770             * to -1 and make sure a backwards/forwards search starts at last/first
1771             * row, respectively.
1772             * 
1773             * @param startIndex
1774             * @param backwards
1775             * @return adjusted <code>startIndex</code>
1776             */
1777            @Override
1778            protected int adjustStartPosition(int startIndex, boolean backwards) {
1779                lastSearchResult.foundColumn = -1;
1780                return super.adjustStartPosition(startIndex, backwards);
1781            }
1782    
1783            /**
1784             * Moves the internal start for matching as appropriate and returns the
1785             * new startIndex to use. Called if search was messaged with the same
1786             * startIndex as previously.
1787             * 
1788             * @param startRow
1789             * @param backwards
1790             * @return new start index to use
1791             */
1792            @Override
1793            protected int moveStartPosition(int startRow, boolean backwards) {
1794                if (backwards) {
1795                    lastSearchResult.foundColumn--;
1796                    if (lastSearchResult.foundColumn < 0) {
1797                        startRow--;
1798                    }
1799                } else {
1800                    lastSearchResult.foundColumn++;
1801                    if (lastSearchResult.foundColumn >= getColumnCount()) {
1802                        lastSearchResult.foundColumn = -1;
1803                        startRow++;
1804                    }
1805                }
1806                return startRow;
1807            }
1808    
1809            /**
1810             * Checks if the startIndex is a candidate for trying a re-match.
1811             * 
1812             * 
1813             * @param startIndex
1814             * @return true if the startIndex should be re-matched, false if not.
1815             */
1816            @Override
1817            protected boolean isEqualStartIndex(final int startIndex) {
1818                return super.isEqualStartIndex(startIndex)
1819                        && isValidColumn(lastSearchResult.foundColumn);
1820            }
1821    
1822            /**
1823             * checks if row is in range: 0 <= row < getRowCount().
1824             * 
1825             * @param column
1826             * @return true if the column is in range, false otherwise
1827             */
1828            private boolean isValidColumn(int column) {
1829                return column >= 0 && column < getColumnCount();
1830            }
1831    
1832    
1833            protected int getSize() {
1834                return getRowCount();
1835            }
1836    
1837            protected void moveMatchMarker() {
1838                int row = lastSearchResult.foundRow;
1839                int column = lastSearchResult.foundColumn;
1840                Pattern pattern = lastSearchResult.pattern;
1841                if ((row < 0) || (column < 0)) {
1842                    if (markByHighlighter()) {
1843                        getSearchHighlighter().setPattern(null);
1844                    }
1845                    return;
1846                }
1847                if (markByHighlighter()) {
1848                    Rectangle cellRect = getCellRect(row, column, true);
1849                    if (cellRect != null) {
1850                        scrollRectToVisible(cellRect);
1851                    }
1852                    ensureInsertedSearchHighlighters();
1853                    // TODO (JW) - cleanup SearchHighlighter state management
1854                    getSearchHighlighter().setPattern(pattern);
1855                    int modelColumn = convertColumnIndexToModel(column);
1856                    getSearchHighlighter().setHighlightCell(row, modelColumn);
1857                } else { // use selection
1858                    changeSelection(row, column, false, false);
1859                    if (!getAutoscrolls()) {
1860                        // scrolling not handled by moving selection
1861                        Rectangle cellRect = getCellRect(row, column, true);
1862                        if (cellRect != null) {
1863                            scrollRectToVisible(cellRect);
1864                        }
1865                    }
1866                }
1867            }
1868    
1869            private boolean markByHighlighter() {
1870                return Boolean.TRUE.equals(getClientProperty(MATCH_HIGHLIGHTER));
1871            }
1872    
1873            private SearchHighlighter getSearchHighlighter() {
1874                if (searchHighlighter == null) {
1875                    searchHighlighter = createSearchHighlighter();
1876                }
1877                return searchHighlighter;
1878            }
1879    
1880            private void ensureInsertedSearchHighlighters() {
1881                if (getHighlighters() == null) {
1882                    setHighlighters(new HighlighterPipeline(
1883                            new Highlighter[] { getSearchHighlighter() }));
1884                } else if (!isInPipeline(getSearchHighlighter())) {
1885                    getHighlighters().addHighlighter(getSearchHighlighter());
1886                }
1887            }
1888    
1889            private boolean isInPipeline(PatternHighlighter searchHighlighter) {
1890                Highlighter[] inPipeline = getHighlighters().getHighlighters();
1891                if ((inPipeline.length > 0) && 
1892                   (searchHighlighter.equals(inPipeline[inPipeline.length -1]))) {
1893                    return true;
1894                }
1895                getHighlighters().removeHighlighter(searchHighlighter);
1896                return false;
1897            }
1898    
1899            protected SearchHighlighter createSearchHighlighter() {
1900                return new SearchHighlighter();
1901            }
1902    
1903        }
1904    //-------------------------------- sizing/scrolling support
1905    
1906        /**
1907         * Scrolls vertically to make the given row visible.
1908         * This might not have any effect if the table isn't contained
1909         * in a JViewport. <p>
1910         * 
1911         * Note: this method has no precondition as it internally uses
1912         * getCellRect which is lenient to off-range coordinates.
1913         * 
1914         * @param row the view row index of the cell
1915         */
1916        public void scrollRowToVisible(int row) {
1917            Rectangle cellRect = getCellRect(row, 0, false);
1918            Rectangle visibleRect = getVisibleRect();
1919            cellRect.x = visibleRect.x;
1920            cellRect.width = visibleRect.width;
1921            scrollRectToVisible(cellRect);
1922        }
1923    
1924        /**
1925         * Scrolls horizontally to make the given column visible.
1926         * This might not have any effect if the table isn't contained
1927         * in a JViewport. <p>
1928         * 
1929         * Note: this method has no precondition as it internally uses
1930         * getCellRect which is lenient to off-range coordinates.
1931         * 
1932         * @param column the view column index of the cell
1933         */
1934        public void scrollColumnToVisible(int column) {
1935            Rectangle cellRect = getCellRect(0, column, false);
1936            Rectangle visibleRect = getVisibleRect();
1937            cellRect.y = visibleRect.y;
1938            cellRect.height = visibleRect.height;
1939            scrollRectToVisible(cellRect);
1940        }
1941        
1942    
1943        /**
1944         * Scrolls to make the cell at row and column visible.
1945         * This might not have any effect if the table isn't contained
1946         * in a JViewport.<p>
1947         * 
1948         * Note: this method has no precondition as it internally uses
1949         * getCellRect which is lenient to off-range coordinates.
1950         * 
1951         * @param row the view row index of the cell
1952         * @param column the view column index of the cell
1953         */
1954        public void scrollCellToVisible(int row, int column) {
1955            Rectangle cellRect = getCellRect(row, column, false);
1956            scrollRectToVisible(cellRect);
1957        }
1958    
1959        /** ? */
1960        public void setVisibleRowCount(int visibleRowCount) {
1961            this.visibleRowCount = visibleRowCount;
1962        }
1963    
1964        /** ? */
1965        public int getVisibleRowCount() {
1966            return visibleRowCount;
1967        }
1968    
1969        @Override
1970        public Dimension getPreferredScrollableViewportSize() {
1971            Dimension prefSize = super.getPreferredScrollableViewportSize();
1972    
1973            // JTable hardcodes this to 450 X 400, so we'll calculate it
1974            // based on the preferred widths of the columns and the
1975            // visibleRowCount property instead...
1976    
1977            if (prefSize.getWidth() == 450 && prefSize.getHeight() == 400) {
1978                TableColumnModel columnModel = getColumnModel();
1979                int columnCount = columnModel.getColumnCount();
1980    
1981                int w = 0;
1982                for (int i = 0; i < columnCount; i++) {
1983                    TableColumn column = columnModel.getColumn(i);
1984                    initializeColumnPreferredWidth(column);
1985                    w += column.getPreferredWidth();
1986                }
1987                prefSize.width = w;
1988                JTableHeader header = getTableHeader();
1989                // remind(aim): height is still off...???
1990                int rowCount = getVisibleRowCount();
1991                prefSize.height = rowCount * getRowHeight()
1992                        + (header != null ? header.getPreferredSize().height : 0);
1993                setPreferredScrollableViewportSize(prefSize);
1994            }
1995            return prefSize;
1996        }
1997    
1998        /**
1999         * Packs all the columns to their optimal size. Works best with auto
2000         * resizing turned off.
2001         * 
2002         * Contributed by M. Hillary (Issue #60)
2003         * 
2004         * @param margin
2005         *            the margin to apply to each column.
2006         */
2007        public void packTable(int margin) {
2008            for (int c = 0; c < getColumnCount(); c++)
2009                packColumn(c, margin, -1);
2010        }
2011    
2012        /**
2013         * Packs an indivudal column in the table. Contributed by M. Hillary (Issue
2014         * #60)
2015         * 
2016         * @param column
2017         *            The Column index to pack in View Coordinates
2018         * @param margin
2019         *            The Margin to apply to the column width.
2020         */
2021        public void packColumn(int column, int margin) {
2022            packColumn(column, margin, -1);
2023        }
2024    
2025        /**
2026         * Packs an indivual column in the table to less than or equal to the
2027         * maximum witdth. If maximun is -1 then the column is made as wide as it
2028         * needs. Contributed by M. Hillary (Issue #60)
2029         * 
2030         * @param column
2031         *            The Column index to pack in View Coordinates
2032         * @param margin
2033         *            The margin to apply to the column
2034         * @param max
2035         *            The maximum width the column can be resized to. -1 mean any
2036         *            size.
2037         */
2038        public void packColumn(int column, int margin, int max) {
2039            getColumnFactory().packColumn(this, getColumnExt(column), margin, max);
2040        }
2041    
2042        /**
2043         * Initialize the preferredWidth of the specified column based on the
2044         * column's prototypeValue property. If the column is not an instance of
2045         * <code>TableColumnExt</code> or prototypeValue is <code>null</code>
2046         * then the preferredWidth is left unmodified.
2047         * 
2048         * @see org.jdesktop.swingx.table.TableColumnExt#setPrototypeValue
2049         * @param column
2050         *            TableColumn object representing view column
2051         */
2052        protected void initializeColumnPreferredWidth(TableColumn column) {
2053            if (column instanceof TableColumnExt) {
2054                getColumnFactory().configureColumnWidths(this,
2055                        (TableColumnExt) column);
2056            }
2057        }
2058    
2059        
2060    //----------------------------------- uniform data model access
2061        
2062        protected ComponentAdapter getComponentAdapter() {
2063            if (dataAdapter == null) {
2064                dataAdapter = new TableAdapter(this);
2065            }
2066            return dataAdapter;
2067        }
2068    
2069        
2070        protected static class TableAdapter extends ComponentAdapter {
2071            private final JXTable table;
2072    
2073            /**
2074             * Constructs a <code>TableDataAdapter</code> for the specified target
2075             * component.
2076             * 
2077             * @param component
2078             *            the target component
2079             */
2080            public TableAdapter(JXTable component) {
2081                super(component);
2082                table = component;
2083            }
2084    
2085            /**
2086             * Typesafe accessor for the target component.
2087             * 
2088             * @return the target component as a {@link javax.swing.JTable}
2089             */
2090            public JXTable getTable() {
2091                return table;
2092            }
2093    
2094    
2095            public String getColumnName(int columnIndex) {
2096                TableColumn column = getColumnByModelIndex(columnIndex);
2097                return column == null ? "" : column.getHeaderValue().toString();
2098            }
2099    
2100            protected TableColumn getColumnByModelIndex(int modelColumn) {
2101                List columns = table.getColumns(true);
2102                for (Iterator iter = columns.iterator(); iter.hasNext();) {
2103                    TableColumn column = (TableColumn) iter.next();
2104                    if (column.getModelIndex() == modelColumn) {
2105                        return column;
2106                    }
2107                }
2108                return null;
2109            }
2110    
2111            
2112            public String getColumnIdentifier(int columnIndex) {
2113                
2114                TableColumn column = getColumnByModelIndex(columnIndex);
2115                Object identifier = column != null ? column.getIdentifier() : null;
2116                return identifier != null ? identifier.toString() : null;
2117            }
2118            
2119            @Override
2120            public int getColumnCount() {
2121                return table.getModel().getColumnCount();
2122            }
2123    
2124            @Override
2125            public int getRowCount() {
2126                return table.getModel().getRowCount();
2127            }
2128    
2129            /**
2130             * {@inheritDoc}
2131             */
2132            public Object getValueAt(int row, int column) {
2133                return table.getModel().getValueAt(row, column);
2134            }
2135    
2136            public void setValueAt(Object aValue, int row, int column) {
2137                table.getModel().setValueAt(aValue, row, column);
2138            }
2139    
2140            public boolean isCellEditable(int row, int column) {
2141                return table.getModel().isCellEditable(row, column);
2142            }
2143    
2144            
2145            
2146            @Override
2147            public boolean isTestable(int column) {
2148                return getColumnByModelIndex(column) != null;
2149            }
2150    //-------------------------- accessing view state/values
2151            
2152            public Object getFilteredValueAt(int row, int column) {
2153                return getValueAt(table.convertRowIndexToModel(row), column);
2154    //            return table.getValueAt(row, modelToView(column)); // in view coordinates
2155            }
2156    
2157            /**
2158             * {@inheritDoc}
2159             */
2160            public boolean isSelected() {
2161                return table.isCellSelected(row, column);
2162            }
2163            /**
2164             * {@inheritDoc}
2165             */
2166            public boolean hasFocus() {
2167                boolean rowIsLead = (table.getSelectionModel()
2168                        .getLeadSelectionIndex() == row);
2169                boolean colIsLead = (table.getColumnModel().getSelectionModel()
2170                        .getLeadSelectionIndex() == column);
2171                return table.isFocusOwner() && (rowIsLead && colIsLead);
2172            }
2173    
2174            /**
2175             * {@inheritDoc}
2176             */
2177            @Override
2178            public int modelToView(int columnIndex) {
2179                return table.convertColumnIndexToView(columnIndex);
2180            }
2181    
2182            /**
2183             * {@inheritDoc}
2184             */
2185            @Override
2186            public int viewToModel(int columnIndex) {
2187                return table.convertColumnIndexToModel(columnIndex);
2188            }
2189    
2190    
2191        }
2192    
2193     
2194    //--------------------- managing renderers/editors
2195        
2196        /** Returns the HighlighterPipeline assigned to the table, null if none. */
2197        public HighlighterPipeline getHighlighters() {
2198            return highlighters;
2199        }
2200    
2201        /**
2202         * Assigns a HighlighterPipeline to the table. bound property.
2203         */
2204        public void setHighlighters(HighlighterPipeline pipeline) {
2205            HighlighterPipeline old = getHighlighters();
2206            if (old != null) {
2207                old.removeChangeListener(getHighlighterChangeListener());
2208            }
2209            highlighters = pipeline;
2210            if (highlighters != null) {
2211                highlighters.addChangeListener(getHighlighterChangeListener());
2212            }
2213            firePropertyChange("highlighters", old, getHighlighters());
2214            repaint();
2215        }
2216        
2217        /**
2218         * Adds a Highlighter.
2219         * 
2220         * If the HighlighterPipeline returned from getHighlighters() is null, creates
2221         * and sets a new pipeline containing the given Highlighter. Else, appends
2222         * the Highlighter to the end of the pipeline.
2223         * 
2224         * @param highlighter the Highlighter to add - must not be null.
2225         * @throws NullPointerException if highlighter is null.
2226         */
2227        public void addHighlighter(Highlighter highlighter) {
2228            HighlighterPipeline pipeline = getHighlighters();
2229            if (pipeline == null) {
2230               setHighlighters(new HighlighterPipeline(new Highlighter[] {highlighter})); 
2231            } else {
2232                pipeline.addHighlighter(highlighter);
2233            }
2234        }
2235    
2236        /**
2237         * Removes the Highlighter.
2238         * 
2239         * Does nothing if the HighlighterPipeline is null or does not contain
2240         * the given Highlighter.
2241         * 
2242         * @param highlighter the highlighter to remove.
2243         */
2244        public void removeHighlighter(Highlighter highlighter) {
2245            if ((getHighlighters() == null)) return;
2246            getHighlighters().removeHighlighter(highlighter);
2247        }
2248        
2249        /**
2250         * returns the ChangeListener to use with highlighters. Creates one if
2251         * necessary.
2252         * 
2253         * @return != null
2254         */
2255        private ChangeListener getHighlighterChangeListener() {
2256            if (highlighterChangeListener == null) {
2257                highlighterChangeListener = new ChangeListener() {
2258    
2259                    public void stateChanged(ChangeEvent e) {
2260                        repaint();
2261    
2262                    }
2263    
2264                };
2265            }
2266            return highlighterChangeListener;
2267        }
2268    
2269    
2270        
2271        /**
2272         * Returns the decorated <code>Component</code> used as a stamp to render
2273         * the specified cell. Overrides superclass version to provide support for
2274         * cell decorators. 
2275         * 
2276         * Adjusts component orientation (guaranteed to happen before applying 
2277         * Highlighters).
2278         * see - https://swingx.dev.java.net/issues/show_bug.cgi?id=145
2279         * 
2280         * @param renderer
2281         *            the <code>TableCellRenderer</code> to prepare
2282         * @param row
2283         *            the row of the cell to render, where 0 is the first row
2284         * @param column
2285         *            the column of the cell to render, where 0 is the first column
2286         * @return the decorated <code>Component</code> used as a stamp to render
2287         *         the specified cell
2288         * @see org.jdesktop.swingx.decorator.Highlighter
2289         */
2290        public Component prepareRenderer(TableCellRenderer renderer, int row,
2291                int column) {
2292            Component stamp = super.prepareRenderer(renderer, row, column);
2293            adjustComponentOrientation(stamp);
2294            if (highlighters == null) {
2295                return stamp; // no need to decorate renderer with highlighters
2296            } else {
2297                // PENDING - JW: code duplication - 
2298                // add method to access component adapter with row/column
2299                // set as needed!
2300                ComponentAdapter adapter = getComponentAdapter();
2301                adapter.row = row;
2302                adapter.column = column;
2303                return highlighters.apply(stamp, adapter);
2304            }
2305        }
2306    
2307        
2308        /**
2309         * Overridden to adjust the editor's component orientation if 
2310         * appropriate.
2311         */
2312        @Override
2313        public Component prepareEditor(TableCellEditor editor, int row, int column) {
2314            Component comp =  super.prepareEditor(editor, row, column);
2315            adjustComponentOrientation(comp);
2316            return comp;
2317        }
2318    
2319        /**
2320         * adjusts the Component's orientation to JXTable's CO if appropriate.
2321         * Here: always.
2322         * 
2323         * @param stamp
2324         */
2325        protected void adjustComponentOrientation(Component stamp) {
2326            if (stamp.getComponentOrientation().equals(getComponentOrientation())) return;
2327            stamp.applyComponentOrientation(getComponentOrientation());
2328        }
2329    
2330        /**
2331         * Returns a new instance of the default renderer for the specified class.
2332         * This differs from <code>getDefaultRenderer()</code> in that it returns
2333         * a <b>new </b> instance each time so that the renderer may be set and
2334         * customized on a particular column.
2335         * 
2336         * PENDING: must not return null!
2337         * 
2338         * @param columnClass
2339         *            Class of value being rendered
2340         * @return TableCellRenderer instance which renders values of the specified
2341         *         type
2342         */
2343        public TableCellRenderer getNewDefaultRenderer(Class columnClass) {
2344            TableCellRenderer renderer = getDefaultRenderer(columnClass);
2345            if (renderer != null) {
2346                try {
2347                    return (TableCellRenderer) renderer.getClass().newInstance();
2348                } catch (Exception e) {
2349                    LOG.fine("could not create renderer for " + columnClass);
2350                }
2351            }
2352            return null;
2353        }
2354    
2355        /**
2356         * Creates default cell renderers for objects, numbers, doubles, dates,
2357         * booleans, icons, and links.
2358         * THINK: delegate to TableCellRenderers?
2359         * Overridden so we can act as factory for renderers plus hacking around
2360         * huge memory consumption of UIDefaults (see #6345050 in core Bug parade)
2361         * 
2362         */
2363        @Override
2364        protected void createDefaultRenderers() {
2365            // super.createDefaultRenderers();
2366            // This duplicates JTable's functionality in order to make the renderers
2367            // available in getNewDefaultRenderer(); If JTable's renderers either
2368            // were public, or it provided a factory for *new* renderers, this would
2369            // not be needed
2370            
2371            // hack around #6345050 - new UIDefaults() 
2372            // is created with a huge initialCapacity
2373            // giving a dummy key/value array as parameter reduces that capacity 
2374            // to length/2.
2375            Object[] dummies = new Object[] {
2376                  1, 0,
2377                  2, 0,
2378                  3, 0,
2379                  4, 0,
2380                  5, 0,
2381                  6, 0,
2382                  7, 0,
2383                  8, 0,
2384                  9, 0,
2385                  10, 0,
2386                  
2387            };
2388            defaultRenderersByColumnClass = new UIDefaults(dummies);
2389            defaultRenderersByColumnClass.clear();
2390    
2391    //        defaultRenderersByColumnClass = new UIDefaults();
2392            // Objects
2393            setLazyRenderer(Object.class,
2394                    "javax.swing.table.DefaultTableCellRenderer");
2395    
2396            // Numbers
2397            setLazyRenderer(Number.class,
2398                    "org.jdesktop.swingx.JXTable$NumberRenderer");
2399    
2400            // Doubles and Floats
2401            setLazyRenderer(Float.class,
2402                    "org.jdesktop.swingx.JXTable$DoubleRenderer");
2403            setLazyRenderer(Double.class,
2404                    "org.jdesktop.swingx.JXTable$DoubleRenderer");
2405    
2406            // Dates
2407            setLazyRenderer(Date.class, "org.jdesktop.swingx.JXTable$DateRenderer");
2408    
2409            // Icons and ImageIcons
2410            setLazyRenderer(Icon.class, "org.jdesktop.swingx.JXTable$IconRenderer");
2411            setLazyRenderer(ImageIcon.class,
2412                    "org.jdesktop.swingx.JXTable$IconRenderer");
2413    
2414            // Booleans
2415            setLazyRenderer(Boolean.class,
2416                    "org.jdesktop.swingx.JXTable$BooleanRenderer");
2417    
2418            // Other
2419    //        setLazyRenderer(LinkModel.class, "org.jdesktop.swingx.LinkRenderer");
2420        }
2421    
2422    
2423        /** ? */
2424        private void setLazyValue(Hashtable h, Class c, String s) {
2425            h.put(c, new UIDefaults.ProxyLazyValue(s));
2426        }
2427    
2428        /** ? */
2429        private void setLazyRenderer(Class c, String s) {
2430            setLazyValue(defaultRenderersByColumnClass, c, s);
2431        }
2432    
2433        /** ? */
2434        private void setLazyEditor(Class c, String s) {
2435            setLazyValue(defaultEditorsByColumnClass, c, s);
2436        }
2437    
2438        /*
2439         * Default Type-based Renderers: JTable's default table cell renderer
2440         * classes are private and JTable:getDefaultRenderer() returns a *shared*
2441         * cell renderer instance, thus there is no way for us to instantiate a new
2442         * instance of one of its default renderers. So, we must replicate the
2443         * default renderer classes here so that we can instantiate them when we
2444         * need to create renderers to be set on specific columns.
2445         */
2446        public static class NumberRenderer extends DefaultTableCellRenderer {
2447            public NumberRenderer() {
2448                super();
2449                setHorizontalAlignment(JLabel.TRAILING);
2450            }
2451        }
2452    
2453        public static class DoubleRenderer extends NumberRenderer {
2454            private final NumberFormat formatter;
2455    
2456            public DoubleRenderer() {
2457                this(null);
2458            }
2459    
2460            public DoubleRenderer(NumberFormat formatter) {
2461                if (formatter == null) {
2462                    formatter = NumberFormat.getInstance();
2463                }
2464                this.formatter = formatter;
2465            }
2466    
2467            public void setValue(Object value) {
2468                setText((value == null) ? "" : formatter.format(value));
2469            }
2470        }
2471    
2472        public static class DateRenderer extends DefaultTableCellRenderer {
2473            private final DateFormat formatter;
2474    
2475            public DateRenderer() {
2476                this(null);
2477            }
2478    
2479            public DateRenderer(DateFormat formatter) {
2480                if (formatter == null) {
2481                    formatter = DateFormat.getDateInstance();
2482                }
2483                this.formatter = formatter;
2484            }
2485    
2486            public void setValue(Object value) {
2487                setText((value == null) ? "" : formatter.format(value));
2488            }
2489        }
2490    
2491        public static class IconRenderer extends DefaultTableCellRenderer {
2492            public IconRenderer() {
2493                super();
2494                setHorizontalAlignment(JLabel.CENTER);
2495            }
2496    
2497            public void setValue(Object value) {
2498                setIcon((value instanceof Icon) ? (Icon) value : null);
2499            }
2500        }
2501    
2502        /*
2503         * re- c&p'd from 1.5 JTable. 
2504         */
2505        public static class BooleanRenderer extends JCheckBox implements // , UIResource
2506                TableCellRenderer     {
2507            private static final Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);
2508    
2509            public BooleanRenderer() {
2510                super();
2511                setHorizontalAlignment(JLabel.CENTER);
2512                setBorderPainted(true);
2513            }
2514    
2515            public Component getTableCellRendererComponent(JTable table,
2516                    Object value, boolean isSelected, boolean hasFocus, int row,
2517                    int column) {
2518                if (isSelected) {
2519                    setForeground(table.getSelectionForeground());
2520                    super.setBackground(table.getSelectionBackground());
2521                } else {
2522                    setForeground(table.getForeground());
2523                    setBackground(table.getBackground());
2524                }
2525                setSelected((value != null && ((Boolean) value).booleanValue()));
2526    
2527                if (hasFocus) {
2528                    setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
2529                } else {
2530                    setBorder(noFocusBorder);
2531                }
2532    
2533                return this;
2534            }
2535        }
2536    
2537    
2538        /**
2539         * Creates default cell editors for objects, numbers, and boolean values.
2540         * Overridden to hacking around
2541         * huge memory consumption of UIDefaults (see #6345050 in core Bug parade)
2542         * @see DefaultCellEditor
2543         */
2544        @Override
2545        protected void createDefaultEditors() {
2546            Object[] dummies = new Object[] {
2547                    1, 0,
2548                    2, 0,
2549                    3, 0,
2550                    4, 0,
2551                    5, 0,
2552                    6, 0,
2553                    7, 0,
2554                    8, 0,
2555                    9, 0,
2556                    10, 0,
2557                    
2558              };
2559              defaultEditorsByColumnClass = new UIDefaults(dummies);
2560              defaultEditorsByColumnClass.clear();
2561    //        defaultEditorsByColumnClass = new UIDefaults();
2562    
2563            // Objects
2564            setLazyEditor(Object.class, "org.jdesktop.swingx.JXTable$GenericEditor");
2565    
2566            // Numbers
2567            setLazyEditor(Number.class, "org.jdesktop.swingx.JXTable$NumberEditor");
2568    
2569            // Booleans
2570            setLazyEditor(Boolean.class, "org.jdesktop.swingx.JXTable$BooleanEditor");
2571    //        setLazyEditor(LinkModel.class, "org.jdesktop.swingx.LinkRenderer");
2572    
2573        }
2574    
2575        /**
2576         * Default Editors
2577         */
2578        public static class GenericEditor extends DefaultCellEditor {
2579    
2580            Class[] argTypes = new Class[]{String.class};
2581            java.lang.reflect.Constructor constructor;
2582            Object value;
2583    
2584            public GenericEditor() {
2585                super(new JTextField());
2586                getComponent().setName("Table.editor");
2587            }
2588    
2589            public boolean stopCellEditing() {
2590                String s = (String)super.getCellEditorValue();
2591                // Here we are dealing with the case where a user
2592                // has deleted the string value in a cell, possibly
2593                // after a failed validation. Return null, so that
2594                // they have the option to replace the value with
2595                // null or use escape to restore the original.
2596                // For Strings, return "" for backward compatibility.
2597                if ("".equals(s)) {
2598                    if (constructor.getDeclaringClass() == String.class) {
2599                        value = s;
2600                    }
2601                    super.stopCellEditing();
2602                }
2603    
2604                try {
2605                    value = constructor.newInstance(new Object[]{s});
2606                }
2607                catch (Exception e) {
2608                    ((JComponent)getComponent()).setBorder(new LineBorder(Color.red));
2609                    return false;
2610                }
2611                return super.stopCellEditing();
2612            }
2613    
2614            public Component getTableCellEditorComponent(JTable table, Object value,
2615                                                     boolean isSelected,
2616                                                     int row, int column) {
2617                this.value = null;
2618                ((JComponent)getComponent()).setBorder(new LineBorder(Color.black));
2619                try {
2620                    Class type = table.getColumnClass(column);
2621                    // Since our obligation is to produce a value which is
2622                    // assignable for the required type it is OK to use the
2623                    // String constructor for columns which are declared
2624                    // to contain Objects. A String is an Object.
2625                    if (type == Object.class) {
2626                        type = String.class;
2627                    }
2628                    constructor = type.getConstructor(argTypes);
2629                }
2630                catch (Exception e) {
2631                    return null;
2632                }
2633                return super.getTableCellEditorComponent(table, value, isSelected, row, column);
2634            }
2635    
2636            public Object getCellEditorValue() {
2637                return value;
2638            }
2639        }
2640    
2641        public static class NumberEditor extends GenericEditor {
2642    
2643            public NumberEditor() {
2644                ((JTextField)getComponent()).setHorizontalAlignment(JTextField.RIGHT);
2645            }
2646        }
2647    
2648        public static class BooleanEditor extends DefaultCellEditor {
2649            public BooleanEditor() {
2650                super(new JCheckBox());
2651                JCheckBox checkBox = (JCheckBox)getComponent();
2652                checkBox.setHorizontalAlignment(JCheckBox.CENTER);
2653            }
2654        }
2655    
2656    
2657    // ---------------------------- updateUI support
2658        
2659        /**
2660         * bug fix: super doesn't update all renderers/editors.
2661         */
2662        public void updateUI() {
2663            super.updateUI();
2664            if (columnControlButton != null) {
2665                columnControlButton.updateUI();
2666            }
2667            for (Enumeration defaultEditors = defaultEditorsByColumnClass
2668                    .elements(); defaultEditors.hasMoreElements();) {
2669                updateEditorUI(defaultEditors.nextElement());
2670            }
2671    
2672            for (Enumeration defaultRenderers = defaultRenderersByColumnClass
2673                    .elements(); defaultRenderers.hasMoreElements();) {
2674                updateRendererUI(defaultRenderers.nextElement());
2675            }
2676            List columns = getColumns(true);
2677            for (Iterator iter = columns.iterator(); iter.hasNext();) {
2678                TableColumn column = (TableColumn) iter.next();
2679                updateEditorUI(column.getCellEditor());
2680                updateRendererUI(column.getCellRenderer());
2681                updateRendererUI(column.getHeaderRenderer());
2682            }
2683            updateRowHeightUI(true);
2684            updateHighlighters();
2685            configureViewportBackground();
2686        }
2687    
2688        protected void updateHighlighters() {
2689            if (getHighlighters() == null) return;
2690            getHighlighters().updateUI();
2691        }
2692    
2693        /** ? */
2694        private void updateRowHeightUI(boolean respectRowSetFlag) {
2695            if (respectRowSetFlag && isXTableRowHeightSet)
2696                return;
2697            int minimumSize = getFont().getSize() + 6;
2698            int uiSize = UIManager.getInt(UIPREFIX + "rowHeight");
2699            setRowHeight(Math.max(minimumSize, uiSize != 0 ? uiSize : 18));
2700            isXTableRowHeightSet = false;
2701        }
2702    
2703        /** Changes the row height for all rows in the table. */
2704        public void setRowHeight(int rowHeight) {
2705            super.setRowHeight(rowHeight);
2706            if (rowHeight > 0) {
2707                isXTableRowHeightSet = true;
2708            }
2709            updateViewSizeSequence();
2710    
2711        }
2712    
2713        
2714        public void setRowHeight(int row, int rowHeight) {
2715            if (!isRowHeightEnabled()) return;
2716            super.setRowHeight(row, rowHeight);
2717            updateViewSizeSequence();
2718            resizeAndRepaint();
2719        }
2720    
2721        /**
2722         * sets enabled state of individual rowHeight support. The default 
2723         * is false.
2724         * Enabling the support envolves reflective access
2725         * to super's private field rowModel which may fail due to security
2726         * issues. If failing the support is not enabled.
2727         * 
2728         * PENDING: should we throw an Exception if the enabled fails? 
2729         * Or silently fail - depends on runtime context, 
2730         * can't do anything about it.
2731         * 
2732         * @param enabled
2733         */
2734        public void setRowHeightEnabled(boolean enabled) {
2735            boolean old = isRowHeightEnabled();
2736            if (old == enabled) return;
2737            if (enabled && !canEnableRowHeight()) return;
2738            rowHeightEnabled = enabled;
2739            if (!enabled) {
2740                adminSetRowHeight(getRowHeight());
2741            }
2742            firePropertyChange("rowHeightEnabled", old, rowHeightEnabled);
2743        }
2744        
2745        private boolean canEnableRowHeight() {
2746            return getRowModelField() != null;
2747        }
2748    
2749        public boolean isRowHeightEnabled() {
2750            return rowHeightEnabled;
2751        }
2752    
2753        private SizeSequence getSuperRowModel() {
2754            try {
2755                Field field = getRowModelField();
2756                if (field != null) {
2757                    return (SizeSequence) field.get(this);
2758                }
2759            } catch (SecurityException e) {
2760                LOG.fine("cannot use reflection " +
2761                " - expected behaviour in sandbox");
2762            } catch (IllegalArgumentException e) {
2763                LOG.fine("problem while accessing super's private field - private api changed?");
2764            } catch (IllegalAccessException e) {
2765                LOG.fine("cannot access private field " +
2766                    " - expected behaviour in sandbox. " +
2767                    "Could be program logic running wild in unrestricted contexts");
2768            }
2769            return null;
2770        }
2771    
2772        /**
2773         * @return <code>Field</code>
2774         */
2775        private Field getRowModelField() {
2776            if (rowModelField == null) {
2777                try {
2778                    rowModelField = JTable.class.getDeclaredField("rowModel");
2779                    rowModelField.setAccessible(true);
2780                } catch (SecurityException e) {
2781                    rowModelField = null;
2782                    LOG.fine("cannot access JTable private field rowModel " +
2783                                    "- expected behaviour in sandbox");
2784                } catch (NoSuchFieldException e) {
2785                    LOG.fine("problem while accessing super's private field" +
2786                                    " - private api changed?");
2787                }
2788            }
2789            return rowModelField;
2790        }
2791        
2792        /**
2793         * 
2794         * @return <code>SizeSequenceMapper</code>
2795         */
2796        protected SizeSequenceMapper getRowModelMapper() {
2797            if (rowModelMapper == null) {
2798                rowModelMapper = new SizeSequenceMapper(filters);
2799            }
2800            return rowModelMapper;
2801        }
2802    
2803        /**
2804         * calling setRowHeight for internal reasons.
2805         * Keeps the isXTableRowHeight unchanged.
2806         */
2807        protected void adminSetRowHeight(int rowHeight) {
2808            boolean heightSet = isXTableRowHeightSet;
2809            setRowHeight(rowHeight); 
2810            isXTableRowHeightSet = heightSet;
2811        }
2812    
2813    
2814        private void updateEditorUI(Object value) {
2815            // maybe null or proxyValue
2816            if (!(value instanceof TableCellEditor))
2817                return;
2818            // super handled this
2819            if ((value instanceof JComponent)
2820                    || (value instanceof DefaultCellEditor))
2821                return;
2822            // custom editors might balk about fake rows/columns
2823            try {
2824                Component comp = ((TableCellEditor) value)
2825                        .getTableCellEditorComponent(this, null, false, -1, -1);
2826                if (comp instanceof JComponent) {
2827                    ((JComponent) comp).updateUI();
2828                }
2829            } catch (Exception e) {
2830                // ignore - can't do anything
2831            }
2832        }
2833    
2834        /** ? */
2835        private void updateRendererUI(Object value) {
2836            // maybe null or proxyValue
2837            if (!(value instanceof TableCellRenderer))
2838                return;
2839            // super handled this
2840            if (value instanceof JComponent)
2841                return;
2842            // custom editors might balk about fake rows/columns
2843            try {
2844                Component comp = ((TableCellRenderer) value)
2845                        .getTableCellRendererComponent(this, null, false, false,
2846                                -1, -1);
2847                if (comp instanceof JComponent) {
2848                    ((JComponent) comp).updateUI();
2849                }
2850            } catch (Exception e) {
2851                // ignore - can't do anything
2852            }
2853        }
2854    
2855    
2856        
2857    //---------------------------- overriding super factory methods and buggy
2858        /**
2859         * workaround bug in JTable. (Bug Parade ID #6291631 - negative y is mapped
2860         * to row 0).
2861         */
2862        public int rowAtPoint(Point point) {
2863            if (point.y < 0)
2864                return -1;
2865            return super.rowAtPoint(point);
2866        }
2867    
2868        
2869        /** ? */
2870        protected JTableHeader createDefaultTableHeader() {
2871            return new JXTableHeader(columnModel);
2872        }
2873    
2874        /** ? */
2875        protected TableColumnModel createDefaultColumnModel() {
2876            return new DefaultTableColumnModelExt();
2877        }
2878    
2879        
2880    }