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 "sortable" 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 >= 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 }