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 }