|
XJTable |
|
1 /* 2 * Copyright (c) 1998-2001, The University of Sheffield. 3 * 4 * This file is part of GATE (see http://gate.ac.uk/), and is free 5 * software, licenced under the GNU Library General Public License, 6 * Version 2, June 1991 (in the distribution as file licence.html, 7 * and also available at http://gate.ac.uk/gate/licence.html). 8 * 9 * Valentin Tablan 23/01/2001 10 * 11 * $Id: XJTable.java,v 1.11 2001/11/16 17:32:22 valyt Exp $ 12 * 13 */ 14 15 package gate.swing; 16 17 import java.awt.*; 18 import java.awt.event.*; 19 import java.util.*; 20 import javax.swing.*; 21 import javax.swing.table.*; 22 import javax.swing.event.*; 23 24 import gate.util.*; 25 26 /** 27 * A "smarter" JTable. Feaures include: 28 * <ul> 29 * <li>sorting the table using the values from a column as keys</li> 30 * <li>updating the widths of the columns so they accommodate the contents to 31 * their preferred sizes. 32 * </ul> 33 * It uses a custom made model that stands between the table model set by the 34 * user and the gui component. This middle model is responsible for sorting the 35 * rows. 36 */ 37 public class XJTable extends JTable { 38 39 /**Default constructor*/ 40 public XJTable() { 41 init(); 42 } 43 44 /**Constructor from model*/ 45 public XJTable(TableModel model) { 46 init(); 47 setModel(model); 48 } 49 50 public void setModel(TableModel model){ 51 if(sorter != null) sorter.setModel(model); 52 else{ 53 sorter = new TableSorter(model); 54 super.setModel(sorter); 55 } 56 }// void setModel(TableModel model) 57 58 /** 59 * Returns the actual table model. Note that gateModel() will return the 60 * middle model used for sorting. This cannot be avoided because JTable 61 * expects to find the model used for the component when calling getModel(). 62 */ 63 public TableModel getActualModel(){ 64 if(sorter != null)return sorter.getModel(); 65 else return super.getModel(); 66 }// public TableModel getActualModel() 67 68 /** 69 * Get the row in the table for a row in the model. 70 */ 71 public int getTableRow(int modelRow){ 72 for(int i = 0; i < sorter.indexes.length; i ++){ 73 if(sorter.indexes[i] == modelRow) return i; 74 } 75 return -1; 76 } 77 78 public void tableChanged(TableModelEvent e){ 79 super.tableChanged(e); 80 adjustSizes(); 81 } 82 83 /**Should the soring facility be enabled*/ 84 public void setSortable(boolean isSortable){ 85 this.sortable = isSortable; 86 } 87 88 protected void init(){ 89 //make sure we have a model 90 if(sorter == null){ 91 sorter = new TableSorter(super.getModel()); 92 super.setModel(sorter); 93 } 94 //read the arrows icons 95 upIcon = new ImageIcon(getClass().getResource(Files.getResourcePath() + 96 "/img/up.gif")); 97 downIcon = new ImageIcon(getClass().getResource(Files.getResourcePath() + 98 "/img/down.gif")); 99 100 setColumnSelectionAllowed(false); 101 headerMouseListener = new MouseAdapter() { 102 public void mouseClicked(MouseEvent e) { 103 if(!sortable) return; 104 TableColumnModel columnModel = getColumnModel(); 105 int viewColumn = columnModel.getColumnIndexAtX(e.getX()); 106 int column = convertColumnIndexToModel(viewColumn); 107 if (column != -1) { 108 if(column != sortedColumn) ascending = true; 109 else ascending = !ascending; 110 sorter.sortByColumn(column); 111 sortedColumn = column; 112 } 113 adjustSizes(); 114 } 115 }; 116 if(sortable) getTableHeader().addMouseListener(headerMouseListener); 117 setAutoResizeMode(AUTO_RESIZE_OFF); 118 headerRenderer = 119 new CustomHeaderRenderer(getTableHeader().getDefaultRenderer()); 120 121 getTableHeader().setDefaultRenderer(headerRenderer); 122 }//init() 123 124 125 protected void configureEnclosingScrollPane(){ 126 super.configureEnclosingScrollPane(); 127 //if we're into a scroll pane resize with it 128 Container p = getParent(); 129 if (p instanceof JViewport) { 130 Container gp = p.getParent(); 131 if (gp instanceof JScrollPane) { 132 JScrollPane scrollPane = (JScrollPane)gp; 133 // Make certain we are the viewPort's view and not, for 134 // example, the rowHeaderView of the scrollPane - 135 // an implementor of fixed columns might do this. 136 JViewport viewport = scrollPane.getViewport(); 137 if (viewport != null && viewport.getView() == this) { 138 scrollPane.addComponentListener(new ComponentAdapter() { 139 public void componentResized(ComponentEvent e) { 140 adjustSizes(); 141 } 142 143 public void componentShown(ComponentEvent e) { 144 adjustSizes(); 145 } 146 }); 147 }//if 148 }//if 149 }//if 150 }// void configureEnclosingScrollPane() 151 152 153 /**Resizes all the cells so they accommodate the components at their 154 * preferred sizes. 155 */ 156 protected void adjustSizes(){ 157 int totalWidth = 0; 158 TableColumn tCol = null; 159 Dimension dim; 160 int cellWidth; 161 int cellHeight; 162 int rowMargin = getRowMargin(); 163 164 //delete the current rowModel in order to get a new updated one 165 //this way we fix a bug in JTable 166 setRowHeight(Math.max(getRowHeight(0), 1)); 167 for(int column = 0; column < getColumnCount(); column ++){ 168 int width; 169 tCol = getColumnModel().getColumn(column); 170 //compute the sizes 171 width = headerRenderer.getTableCellRendererComponent( 172 this, tCol.getHeaderValue(), false, false,0,column 173 ).getPreferredSize().width; 174 for(int row = 0; row < getRowCount(); row ++){ 175 TableCellRenderer renderer = getCellRenderer(row,column); 176 if(renderer == null){ 177 renderer = getDefaultRenderer(getModel().getColumnClass(column)); 178 } 179 if(renderer != null){ 180 dim = renderer. 181 getTableCellRendererComponent( 182 this, getValueAt(row, column), false, false, row, column 183 ).getPreferredSize(); 184 cellWidth = dim.width; 185 cellHeight = dim.height; 186 width = Math.max(width, cellWidth); 187 //width = Math.max(width, tCol.getPreferredWidth()); 188 if((cellHeight + rowMargin) > getRowHeight(row)){ 189 setRowHeight(row, cellHeight + rowMargin); 190 } 191 }//if(renderer != null) 192 }//for 193 194 width += getColumnModel().getColumnMargin(); 195 tCol.setPreferredWidth(width); 196 tCol.setWidth(width); 197 totalWidth += width; 198 } 199 int totalHeight = 0; 200 for (int row = 0; row < getRowCount(); row++) 201 totalHeight += getRowHeight(row); 202 dim = new Dimension(totalWidth, totalHeight); 203 setPreferredScrollableViewportSize(dim); 204 205 //extend the last column 206 Container p = getParent(); 207 if (p instanceof JViewport) { 208 Container gp = p.getParent(); 209 if (gp instanceof JScrollPane) { 210 JScrollPane scrollPane = (JScrollPane)gp; 211 // Make certain we are the viewPort's view and not, for 212 // example, the rowHeaderView of the scrollPane - 213 // an implementor of fixed columns might do this. 214 JViewport viewport = scrollPane.getViewport(); 215 if (viewport == null || viewport.getView() != this) { 216 return; 217 } 218 int portWidth = scrollPane.getSize().width - 219 scrollPane.getInsets().left - 220 scrollPane.getInsets().right; 221 if(scrollPane.getVerticalScrollBar().isVisible()) 222 portWidth -= scrollPane.getVerticalScrollBar().getWidth(); 223 if(totalWidth < portWidth){ 224 int width = tCol.getWidth() + portWidth - totalWidth - 2; 225 tCol.setPreferredWidth(width); 226 tCol.setWidth(width); 227 }//if(totalWidth < portWidth) 228 }//if (gp instanceof JScrollPane) 229 }//if (p instanceof JViewport) 230 }//protected void adjustSizes() 231 232 /** 233 * Sets the column to be used as key for sorting. This column changes 234 * automatically when the user click a column header. 235 */ 236 public void setSortedColumn(int column){ 237 sortedColumn = column; 238 sorter.sortByColumn(sortedColumn); 239 } 240 241 /**Should the sorting be ascending or descending*/ 242 public void setAscending(boolean ascending){ 243 this.ascending = ascending; 244 } 245 246 public void setAutoResizeMode(int resizeMode){ 247 /* 248 throw new UnsupportedOperationException( 249 "Auto resize mode not supported for " + getClass().getName() + ".\n" 250 "The default mode is AUTO_RESIZE_LAST_COLUMN"); 251 */ 252 } 253 254 protected TableSorter sorter; 255 256 protected Icon upIcon; 257 protected Icon downIcon; 258 int sortedColumn = -1; 259 // int oldSortedColumn = -1; 260 boolean ascending = true; 261 protected TableCellRenderer headerRenderer; 262 protected boolean sortable = true; 263 MouseListener headerMouseListener; 264 // protected TableCellRenderer savedHeaderRenderer; 265 266 //classes 267 268 /** 269 * A sorter for TableModels. The sorter has a model (conforming to TableModel) 270 * and itself implements TableModel. TableSorter does not store or copy 271 * the data in the TableModel, instead it maintains an array of 272 * integers which it keeps the same size as the number of rows in its 273 * model. When the model changes it notifies the sorter that something 274 * has changed eg. "rowsAdded" so that its internal array of integers 275 * can be reallocated. As requests are made of the sorter (like 276 * getValueAt(row, col) it redirects them to its model via the mapping 277 * array. That way the TableSorter appears to hold another copy of the table 278 * with the rows in a different order. The sorting algorthm used is stable 279 * which means that it does not move around rows when its comparison 280 * function returns 0 to denote that they are equivalent. 281 * 282 * @version 1.5 12/17/97 283 * @author Philip Milne 284 */ 285 286 class TableSorter extends TableMap { 287 int indexes[]; 288 Vector sortingColumns = new Vector(); 289 290 public TableSorter() { 291 indexes = new int[0]; // for consistency 292 } 293 294 public TableSorter(TableModel model) { 295 setModel(model); 296 } 297 298 public void setModel(TableModel model) { 299 super.setModel(model); 300 reallocateIndexes(); 301 } 302 303 public int compareRowsByColumn(int row1, int row2, int column) { 304 Class type = model.getColumnClass(column); 305 TableModel data = model; 306 307 // Check for nulls. 308 309 Object o1 = data.getValueAt(row1, column); 310 Object o2 = data.getValueAt(row2, column); 311 312 // If both values are null, return 0. 313 if (o1 == null && o2 == null) { 314 return 0; 315 } else if (o1 == null) { // Define null less than everything. 316 return -1; 317 } else if (o2 == null) { 318 return 1; 319 } 320 321 /* 322 * We copy all returned values from the getValue call in case 323 * an optimised model is reusing one object to return many 324 * values. The Number subclasses in the JDK are immutable and 325 * so will not be used in this way but other subclasses of 326 * Number might want to do this to save space and avoid 327 * unnecessary heap allocation. 328 */ 329 330 if (type.getSuperclass() == java.lang.Number.class) { 331 Number n1 = (Number)data.getValueAt(row1, column); 332 double d1 = n1.doubleValue(); 333 Number n2 = (Number)data.getValueAt(row2, column); 334 double d2 = n2.doubleValue(); 335 336 if (d1 < d2) { 337 return -1; 338 } else if (d1 > d2) { 339 return 1; 340 } else { 341 return 0; 342 } 343 } else if (type == java.util.Date.class) { 344 Date d1 = (Date)data.getValueAt(row1, column); 345 long n1 = d1.getTime(); 346 Date d2 = (Date)data.getValueAt(row2, column); 347 long n2 = d2.getTime(); 348 349 if (n1 < n2) { 350 return -1; 351 } else if (n1 > n2) { 352 return 1; 353 } else { 354 return 0; 355 } 356 } else if (type == String.class) { 357 String s1 = (String)data.getValueAt(row1, column); 358 String s2 = (String)data.getValueAt(row2, column); 359 int result = s1.compareTo(s2); 360 361 if (result < 0) { 362 return -1; 363 } else if (result > 0) { 364 return 1; 365 } else { 366 return 0; 367 } 368 } else if (type == Boolean.class) { 369 Boolean bool1 = (Boolean)data.getValueAt(row1, column); 370 boolean b1 = bool1.booleanValue(); 371 Boolean bool2 = (Boolean)data.getValueAt(row2, column); 372 boolean b2 = bool2.booleanValue(); 373 374 if (b1 == b2) { 375 return 0; 376 } else if (b1) { // Define false < true 377 return 1; 378 } else { 379 return -1; 380 } 381 } else { 382 Object v1 = data.getValueAt(row1, column); 383 Object v2 = data.getValueAt(row2, column); 384 int result; 385 if(v1 instanceof Comparable){ 386 try { 387 result = ((Comparable)v1).compareTo(v2); 388 } catch(ClassCastException cce) { 389 String s1 = v1.toString(); 390 String s2 = v2.toString(); 391 result = s1.compareTo(s2); 392 } 393 } else { 394 String s1 = v1.toString(); 395 String s2 = v2.toString(); 396 result = s1.compareTo(s2); 397 } 398 399 if (result < 0) { 400 return -1; 401 } else if (result > 0) { 402 return 1; 403 } else { 404 return 0; 405 } 406 } 407 } 408 409 public int compare(int row1, int row2) { 410 // compares++; 411 for (int level = 0; level < sortingColumns.size(); level++) { 412 Integer column = (Integer)sortingColumns.elementAt(level); 413 int result = compareRowsByColumn(row1, row2, column.intValue()); 414 if (result != 0) { 415 return ascending ? result : -result; 416 } 417 } 418 return 0; 419 } 420 421 public void reallocateIndexes() { 422 int rowCount = model.getRowCount(); 423 424 // Set up a new array of indexes with the right number of elements 425 // for the new data model. 426 indexes = new int[rowCount]; 427 428 // Initialise with the identity mapping. 429 for (int row = 0; row < rowCount; row++) { 430 indexes[row] = row; 431 } 432 } 433 434 public void tableChanged(TableModelEvent e) { 435 reallocateIndexes(); 436 sort(sorter); 437 super.tableChanged(e); 438 } 439 440 public void checkModel() { 441 if (indexes.length != model.getRowCount()) { 442 tableChanged(null); 443 //System.err.println("Sorter not informed of a change in model."); 444 } 445 } 446 447 public void sort(Object sender) { 448 checkModel(); 449 shuttlesort((int[])indexes.clone(), indexes, 0, indexes.length); 450 } 451 452 // This is a home-grown implementation which we have not had time 453 // to research - it may perform poorly in some circumstances. It 454 // requires twice the space of an in-place algorithm and makes 455 // NlogN assigments shuttling the values between the two 456 // arrays. The number of compares appears to vary between N-1 and 457 // NlogN depending on the initial order but the main reason for 458 // using it here is that, unlike qsort, it is stable. 459 public void shuttlesort(int from[], int to[], int low, int high) { 460 if (high - low < 2) { 461 return; 462 } 463 int middle = (low + high)/2; 464 shuttlesort(to, from, low, middle); 465 shuttlesort(to, from, middle, high); 466 467 int p = low; 468 int q = middle; 469 470 /* This is an optional short-cut; at each recursive call, 471 check to see if the elements in this subset are already 472 ordered. If so, no further comparisons are needed; the 473 sub-array can just be copied. The array must be copied rather 474 than assigned otherwise sister calls in the recursion might 475 get out of sinc. When the number of elements is three they 476 are partitioned so that the first set, [low, mid), has one 477 element and and the second, [mid, high), has two. We skip the 478 optimisation when the number of elements is three or less as 479 the first compare in the normal merge will produce the same 480 sequence of steps. This optimisation seems to be worthwhile 481 for partially ordered lists but some analysis is needed to 482 find out how the performance drops to Nlog(N) as the initial 483 order diminishes - it may drop very quickly. */ 484 485 if (high - low >= 4 && compare(from[middle-1], from[middle]) <= 0) { 486 for (int i = low; i < high; i++) { 487 to[i] = from[i]; 488 } 489 return; 490 } 491 492 // A normal merge. 493 494 for (int i = low; i < high; i++) { 495 if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) { 496 to[i] = from[p++]; 497 } 498 else { 499 to[i] = from[q++]; 500 } 501 } 502 } 503 504 public void swap(int i, int j) { 505 int tmp = indexes[i]; 506 indexes[i] = indexes[j]; 507 indexes[j] = tmp; 508 } 509 510 // The mapping only affects the contents of the data rows. 511 // Pass all requests to these rows through the mapping array: "indexes". 512 513 public Object getValueAt(int aRow, int aColumn) { 514 checkModel(); 515 return model.getValueAt(indexes[aRow], aColumn); 516 } 517 518 public void setValueAt(Object aValue, int aRow, int aColumn) { 519 checkModel(); 520 model.setValueAt(aValue, indexes[aRow], aColumn); 521 } 522 523 public boolean isCellEditable(int aRow, int aColumn) { 524 checkModel(); 525 return model.isCellEditable(indexes[aRow], aColumn); 526 } 527 528 public void sortByColumn(int column) { 529 sortingColumns.removeAllElements(); 530 sortingColumns.addElement(new Integer(column)); 531 sort(this); 532 super.tableChanged(new TableModelEvent(this)); 533 getTableHeader().repaint(); 534 } 535 }//class TableSorter extends TableMap 536 537 class CustomHeaderRenderer extends DefaultTableCellRenderer{ 538 public CustomHeaderRenderer(TableCellRenderer oldRenderer){ 539 this.oldRenderer = oldRenderer; 540 } 541 542 public Component getTableCellRendererComponent(JTable table, 543 Object value, 544 boolean isSelected, 545 boolean hasFocus, 546 int row, 547 int column){ 548 549 Component res = oldRenderer.getTableCellRendererComponent( 550 table, value, isSelected, hasFocus, row, column); 551 if(res instanceof JLabel){ 552 if(convertColumnIndexToModel(column) == sortedColumn){ 553 ((JLabel)res).setIcon(ascending?upIcon:downIcon); 554 } else { 555 ((JLabel)res).setIcon(null); 556 } 557 ((JLabel)res).setHorizontalTextPosition(JLabel.LEFT); 558 } 559 return res; 560 }// Component getTableCellRendererComponent 561 protected TableCellRenderer oldRenderer; 562 563 }// class CustomHeaderRenderer extends DefaultTableCellRenderer 564 }
|
XJTable |
|