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