1 /* 2 * $Id$ 3 * 4 * Copyright (c) 2004, 2011, Oracle and/or its affiliates. All rights reserved. 5 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 6 * 7 * This code is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU General Public License version 2 only, as 9 * published by the Free Software Foundation. Oracle designates this 10 * particular file as subject to the "Classpath" exception as provided 11 * by Oracle in the LICENSE file that accompanied this code. 12 * 13 * This code is distributed in the hope that it will be useful, but WITHOUT 14 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 16 * version 2 for more details (a copy is included in the LICENSE file that 17 * accompanied this code). 18 * 19 * You should have received a copy of the GNU General Public License version 20 * 2 along with this work; if not, write to the Free Software Foundation, 21 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 22 * 23 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 24 * or visit www.oracle.com if you need additional information or have any 25 * questions. 26 */ 27 package com.sun.javatest.exec; 28 29 import java.awt.Component; 30 import java.awt.Dimension; 31 import java.awt.EventQueue; 32 import java.awt.Font; 33 import java.awt.GridBagConstraints; 34 import java.awt.GridBagLayout; 35 import java.awt.event.ActionEvent; 36 import java.awt.event.ActionListener; 37 import java.awt.event.KeyEvent; 38 import java.awt.event.MouseAdapter; 39 import java.awt.event.MouseEvent; 40 import java.util.LinkedList; 41 import java.util.Vector; 42 43 import javax.swing.AbstractAction; 44 import javax.swing.BorderFactory; 45 import javax.swing.JComponent; 46 import javax.swing.JScrollPane; 47 import javax.swing.JTable; 48 import javax.swing.JTextArea; 49 import javax.swing.KeyStroke; 50 import javax.swing.ListSelectionModel; 51 import javax.swing.border.Border; 52 import javax.swing.event.ListSelectionEvent; 53 import javax.swing.event.ListSelectionListener; 54 import javax.swing.event.TableModelEvent; 55 import javax.swing.table.AbstractTableModel; 56 import javax.swing.table.DefaultTableCellRenderer; 57 import javax.swing.table.TableCellRenderer; 58 import javax.swing.table.TableColumn; 59 import javax.swing.table.TableModel; 60 61 import com.sun.javatest.tool.jthelp.ContextHelpManager; 62 import com.sun.javatest.TestFilter; 63 import com.sun.javatest.TestResult; 64 import com.sun.javatest.TestResultTable; 65 import com.sun.javatest.tool.I18NUtils; 66 import com.sun.javatest.tool.UIFactory; 67 import com.sun.javatest.util.Debug; 68 import com.sun.javatest.util.I18NResourceBundle; 69 import com.sun.javatest.util.StringArray; 70 import java.awt.Toolkit; 71 import java.awt.datatransfer.Clipboard; 72 import java.awt.datatransfer.StringSelection; 73 import javax.swing.JPopupMenu; 74 75 /** 76 * This panel renders information about the tests which are "filtered out" in 77 * the current node. It shows the test name, as well as the reason that the 78 * test was filtered out. The tooltip are also upgraded beyond the standard 79 * list to provide more filter information. 80 * <p> 81 * The background thread has a 82 * two-stage commit process so that the iterator can run at full speed, 83 * ignoring the MT-unsafe swing list model. The changes are reflected in the 84 * real list when the AWT event queue schedules the notification thread which 85 * is created during the iteration. This class also processes the list 86 * click events and usually dispatches changes to the branch panel model. 87 * 88 * <p> 89 * If you need to synchronize against both the vLock (for live data) and this 90 * class' lock, then blocks should be synchronized against this outer class, 91 * then the vLock. The ordering is vital to avoiding deadlocks. 92 */ 93 class BP_FilteredOutSubpanel extends BP_BranchSubpanel { 94 BP_FilteredOutSubpanel(UIFactory uif, BP_Model bpm, TestTreeModel ttm) { 95 super("fo", uif, bpm, ttm, "br.fo"); 96 97 init(); 98 ContextHelpManager.setHelpIDString(this, "browse.filteredOutTab.csh"); 99 100 cacheWatcher = new CacheObserver(); 101 } 102 103 /** 104 * Clear the table contents and prepare to receive new data. 105 */ 106 void reset(TT_NodeCache cache) { 107 synchronized (BP_FilteredOutSubpanel.this) { 108 if (this.cache != null) 109 this.cache.removeObserver(cacheWatcher); 110 111 this.cache = cache; 112 113 if (resyncThread != null) { 114 resyncThread.halt(); 115 } 116 117 if (mod != null) 118 mod.reset(); 119 } // sync 120 121 validateEnableState(); 122 //table.clearSelection(); 123 } 124 125 protected void invalidateFilters() { 126 super.invalidateFilters(); 127 128 // if we didn't have one, we certainly don't need to disconnect, 129 // and probably don't need to get a new one... 130 if (cache != null) { 131 cache.removeObserver(cacheWatcher); 132 } 133 134 if (subpanelNode != null) { 135 cache = ttm.getNodeInfo(subpanelNode.getTableNode(), false); 136 validateEnableState(); 137 } 138 139 updateInfoText(); 140 } 141 142 /** 143 * Only called when this panel is onscreen and needs to be kept up to date. 144 */ 145 protected synchronized void updateSubpanel(TT_BasicNode currNode) { 146 super.updateSubpanel(currNode); 147 148 // only run if we change nodes 149 if (lastNode != currNode || filtersInvalidated) { 150 if (debug) 151 Debug.println("updating FO table"); 152 153 if (resyncThread != null) { 154 resyncThread.halt(); 155 } 156 157 resyncThread = new TableSynchronizer(); 158 resyncThread.start(); 159 lastNode = currNode; 160 161 filtersInvalidated = false; 162 validateEnableState(); 163 } 164 } 165 166 private void updateInfoText() { 167 if (infoTa == null) 168 return; 169 170 TestFilter f = model.getFilter(); 171 if (f != null) 172 infoTa.setText(uif.getI18NString("br.fo.info.txt", f.getName())); 173 else 174 infoTa.setText(uif.getI18NString("br.fo.noFn.txt")); 175 } 176 177 /** 178 * Enable or disable this panel as necessary. 179 */ 180 private void validateEnableState() { 181 if (cache.getRejectCount() > 0) { 182 model.setEnabled(BP_FilteredOutSubpanel.this, true); 183 } 184 else if (cache.getRejectCount() == 0) { 185 model.setEnabled(BP_FilteredOutSubpanel.this, false); 186 } 187 else { } 188 } 189 190 /** 191 * A special thread to repopulate the test lists. 192 */ 193 private class TableSynchronizer extends Thread { 194 TableSynchronizer() { 195 super("filtered-out list synchronizer"); 196 setPriority(Thread.MIN_PRIORITY + 2); 197 } 198 199 public void run() { 200 // grab cache lock first because many other threads may alter that 201 // object, causing a deadlock here. 202 // sync. to hold observer traffic until re-sync is done 203 synchronized (cache) { 204 synchronized(BP_FilteredOutSubpanel.this) { 205 // resync with this node cache 206 Vector<TestResult>[] newData = cache.addObserver(cacheWatcher, true); 207 208 // add tests into the list model - this doesn't make the data 209 // live though 210 for (int j = 0; j < newData[newData.length-1].size() - 1; j++) { 211 if (stopping) 212 break; 213 214 mod.addTest(newData[newData.length-1].elementAt(j), true); 215 } // for 216 217 if (newData[newData.length-1].size() > 0 && !stopping) { 218 // final item with a notify 219 mod.addTest(newData[newData.length-1].lastElement(), false); 220 } 221 222 // to indicate completion 223 resyncThread = null; 224 } // this sync 225 } // cache sync 226 227 validateEnableState(); 228 } // run() 229 230 public void halt() { 231 stopping = true; 232 } 233 234 private volatile boolean stopping; 235 } 236 237 private void init() { 238 mod = new TestTableModel(uif); 239 renderer = new TestCellRenderer(uif); 240 listener = new InputListener(); 241 242 table = uif.createTable("br.fo.tbl", mod); 243 table.setOpaque(true); 244 table.setRowSelectionAllowed(true); 245 table.setColumnSelectionAllowed(false); 246 table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 247 table.getSelectionModel().addListSelectionListener(listener); 248 249 // setup col 0 250 TableColumn tc = table.getColumnModel().getColumn(0); 251 tc.setCellRenderer(renderer); 252 tc.setResizable(true); 253 254 // setup col 1 255 tc = table.getColumnModel().getColumn(1); 256 tc.setCellRenderer(renderer); 257 tc.setResizable(true); 258 259 uif.setAccessibleInfo(this, "br.fo"); 260 261 setLayout(new GridBagLayout()); 262 263 GridBagConstraints gbc = new GridBagConstraints(); 264 gbc.fill = GridBagConstraints.HORIZONTAL; 265 gbc.anchor = GridBagConstraints.NORTH; 266 gbc.gridy = 0; 267 gbc.ipady = 12; 268 gbc.weightx = 1.0; 269 gbc.weighty = 0.0; 270 271 infoTa = uif.createMessageArea("br.fo.info"); 272 infoTa.setOpaque(false); 273 add(infoTa, gbc); 274 275 gbc.gridy = 1; 276 gbc.weightx = 2.0; 277 gbc.weighty = 1.0; 278 gbc.fill = GridBagConstraints.BOTH; 279 add(uif.createScrollPane(table, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 280 JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED), gbc); 281 282 updateInfoText(); 283 284 InputListener tableListener = new InputListener(); 285 table.addMouseListener(tableListener); 286 table.getSelectionModel().addListSelectionListener(tableListener); 287 288 // to trigger test selection when enter is pressed 289 table.getInputMap(JComponent.WHEN_FOCUSED).put( 290 KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), 291 "gotoTest"); 292 table.getActionMap().put("gotoTest", 293 new KbTableAction(uif.getI18NResourceBundle(), 294 "br.list.enter")); 295 296 String[] actions = { }; 297 popupTable = uif.createPopupMenu("br", actions, (ActionListener)tableListener); 298 299 actions = new String[] { "action.cpnamelist", "action.cpnamestr" }; 300 popupTable.add(uif.createMenu("br.cp", actions, (ActionListener)tableListener)); 301 302 // this is necessary to make sure that the split pane can resize 303 // this panel. without setting the min., the panel seems to take 304 // all it is given, and never gives it back. 305 setMinimumSize(new Dimension(150,100)); 306 } 307 308 /** 309 * It is assumed that this will run on the event thread. 310 private void setEmpty(boolean state) { 311 if (state && list.getModel() != EmptyListModel.getInstance()) { 312 list.setModel(EmptyListModel.getInstance()); 313 model.setEnabled(BP_TestListSubpanel.this, false); 314 lastMsg = ""; 315 } 316 else if (!state && list.getModel() == EmptyListModel.getInstance()) { 317 list.setModel(mod); 318 model.setEnabled(BP_TestListSubpanel.this, true); 319 } 320 } 321 */ 322 323 // ------------- inner class ------------- 324 /** 325 * Enumerates tree in background to populate the list. 326 * If this thread is running, consider list data incomplete. 327 * Swing cannot handle an updating model, so there is a two-stage absorbtion 328 * of data. This thread runs with no delay reading the TRT and placing that data 329 * into an "offline" queue. It periodically schedules an event on the GUI event 330 * thread; when that thread run, it copies the data from the offline area to the 331 * online area, which is what the ListModel presents. This workaround assumes that 332 * Swing will never dispatch more than one event at a time. 333 */ 334 private class TestTableModel extends AbstractTableModel { 335 TestTableModel(UIFactory uif) { 336 super(); 337 338 colNames = new String[] { 339 uif.getI18NString("br.fo.col0.txt"), 340 uif.getI18NString("br.fo.col1.txt") 341 }; 342 343 if (debug) { 344 Debug.println("TableModel constructed: "); 345 Debug.println(" -> " + this); 346 } 347 348 init(); 349 } 350 351 // ---------- TableModel interface ---------- 352 public int getRowCount() { 353 synchronized (liveData) { 354 return liveData.size(); 355 } 356 } 357 358 public int getColumnCount() { 359 return COLUMN_COUNT; 360 } 361 362 public String getColumnName(int columnIndex) { 363 if (columnIndex >= colNames.length) 364 throw new IndexOutOfBoundsException(); 365 else 366 return colNames[columnIndex]; 367 } 368 369 public Object getValueAt(int row, int column) { 370 if (column == 0) { 371 synchronized (liveData) { 372 return liveData.get(row); 373 } 374 } 375 else if (column == 1) { 376 synchronized (liveData) { 377 Object tst = (liveData.get(row)); 378 Object r = null; 379 380 if (cache != null && r == null) { 381 r = cache.getRejectReason((TestResult)tst); 382 } 383 384 if (r == null) 385 r = uif.getI18NString("br.fo.noFi.txt"); 386 387 return r; 388 } 389 } 390 else 391 throw new IndexOutOfBoundsException( 392 "Index into filtered out table is out of range: " + 393 row + ", " + column); 394 } 395 396 public boolean isCellEditable(int rowIndex, int colIndex) { 397 return false; 398 } 399 400 // ---------- Custom methods for this model ---------- 401 /** 402 * @param suppressNotify Actively request that no update be scheduled. 403 */ 404 void addTest(TestResult tr, boolean suppressNotify) { 405 synchronized (vLock) { 406 // make sure this item is not already in the list 407 if (!inQueue.contains(tr)) { 408 inQueue.addElement(tr); 409 } 410 } // sync 411 412 // try not to saturate the GUI event thread 413 if (!suppressNotify && !isUpdateScheduled) { 414 TableNotifier tn = new TableNotifier(subpanelNode, this); 415 pendingEvents.addElement(tn); 416 EventQueue.invokeLater(tn); 417 } 418 } 419 420 /** 421 * Remove the given test from the list. 422 * Ignored if the test is not in the list. 423 */ 424 void removeTest(TestResult tr) { 425 synchronized (vLock) { 426 rmQueue.addElement(tr); 427 428 // try not to saturate the GUI event thread 429 if (!isUpdateScheduled) { 430 TableNotifier tn = new TableNotifier(subpanelNode, this); 431 pendingEvents.addElement(tn); 432 EventQueue.invokeLater(tn); 433 } 434 } // sync 435 436 } 437 438 void reset() { 439 synchronized (vLock) { 440 init(); 441 } 442 443 // force GUI to update the now empty list 444 notifyDone(); 445 } 446 447 // ------------ private -------------- 448 449 private void init() { 450 // discard all pending events 451 // this is necessary to ensure that update events which haven't 452 // been processed are not processed after the model has changed 453 // arguably, this should be solved by putting this init() onto 454 // the event thread 455 synchronized (pendingEvents) { 456 for (int i = 0; i < pendingEvents.size(); i++) { 457 TableNotifier tn = (TableNotifier)(pendingEvents.get(i)); 458 tn.cancel(); 459 } // for 460 } 461 462 inQueue = new Vector<>(); 463 rmQueue = new Vector<>(); 464 liveData = new LinkedList<>(); 465 466 isUpdateScheduled = false; 467 } 468 469 /** 470 * Transfer data from the internal queue to the live data queue. 471 * This is part of the Swing threading workaround. This method immediately 472 * exits if there is no work to do. It also dispatches model update events 473 * if necessary. 474 * This method always runs on the event dispatch thread. 475 */ 476 private void goLive() { 477 int firstNew, lastNew = 0; 478 if (debug) 479 Debug.println("BP_TL.TLM - goLive() starting."); 480 481 // this is sync. against the outer class because we may change the 482 // list model object during execution of this block 483 synchronized (BP_FilteredOutSubpanel.this) { 484 synchronized (vLock) { 485 if (inQueue.size() == 0 && rmQueue.size() == 0) { 486 if (debug) 487 Debug.println("BP_TT.TTM - goLive() nothing to do, returning"); 488 return; 489 } 490 491 processRemoveQueue(); 492 //preprocessAddQueue(); 493 494 // now add the new items 495 if (inQueue.size() != 0) { 496 synchronized (liveData) { 497 firstNew = liveData.size(); 498 if (inQueue.size() < BATCH_SIZE) { 499 liveData.addAll(inQueue); 500 lastNew = liveData.size()-1; 501 inQueue.setSize(0); 502 } 503 else { // only add some of the new items 504 for (int i = 0; i < BATCH_SIZE; i++) { 505 liveData.add(inQueue.remove(0)); 506 } // for 507 508 // schedule a future update 509 if (!isUpdateScheduled) { 510 TableNotifier tn = new TableNotifier( 511 subpanelNode, this); 512 pendingEvents.addElement(tn); 513 EventQueue.invokeLater(tn); 514 } 515 516 lastNew = liveData.size()-1; 517 } 518 } // sync 519 520 // dispatch update range event to Swing 521 if (listenerList.getListenerCount() > 0) { 522 TableModelEvent e = 523 new TableModelEvent(this, firstNew, lastNew, 524 TableModelEvent.ALL_COLUMNS, 525 TableModelEvent.INSERT); 526 TableNotifier tn = new TableNotifier(e, mod); 527 pendingEvents.addElement(tn); 528 EventQueue.invokeLater(tn); 529 } 530 } 531 532 // enable this tab now that it has data 533 /* 534 if (liveData.size() > 0) { 535 // switch back from an empty list 536 setEmpty(false); 537 } 538 else { 539 setEmpty(true); 540 } 541 */ 542 543 // this clears the "please wait" message if needed 544 if (table.getSelectedRow() == -1 && inQueue.size() == 0) 545 showMessage(""); 546 } // sync 547 } 548 549 if (debug) 550 Debug.println("BP_TL.LT - goLive() finished"); 551 } 552 553 /** 554 * Remove tests in the removal queue from the live data or the incoming data. 555 * vLock should be locked when you call this method 556 */ 557 private void processRemoveQueue() { 558 if (rmQueue.size() == 0) 559 return; 560 561 while (rmQueue.size() > 0) { 562 TestResult target = rmQueue.remove(0); 563 int targetIndex = liveData.indexOf(target); 564 if (targetIndex != -1) { 565 synchronized (liveData) { 566 // necessary for proper synchronization 567 // should not be a problem really, based on how other 568 // locking is done, all work on liveData occurs in goLive() 569 targetIndex = liveData.indexOf(target); 570 571 // only should happen if the item disappears 572 if (targetIndex == -1) 573 continue; 574 575 liveData.remove(targetIndex); 576 577 // WARNING: since we are continually changing the contents of 578 // the data, you must notify the observers synchronously to get 579 // proper results 580 notifyRemoved(target, targetIndex); 581 } // sync 582 } 583 } // while 584 } 585 586 /** 587 * Remove duplicates in the add queue. 588 * vLock should be locked when you call this method 589 */ 590 private void preprocessAddQueue() { 591 // make sure this list does not contain dups 592 for (int i = 0; i < inQueue.size(); i++) { 593 if (liveData.contains(inQueue.elementAt(i))) { 594 inQueue.remove(i); 595 i--; 596 } 597 else { 598 } 599 } // for 600 } 601 602 // --------- event utility methods ----------- 603 /** 604 * Notify observers that the given index was added 605 */ 606 private void notifyAdded(TestResult what, int index) { 607 if (listenerList.getListenerCount() > 0) { 608 // may want to buffer these messages for performance 609 TableModelEvent e = new TableModelEvent (this, index, index, 610 TableModelEvent.ALL_COLUMNS, 611 TableModelEvent.INSERT); 612 613 if (EventQueue.isDispatchThread()) { 614 // XXX try without this to see perf. impact 615 // dispatch synchronously 616 mod.fireTableChanged(e); 617 } 618 else { 619 // switch event onto AWT event thread 620 TableNotifier tn = new TableNotifier(e, mod); 621 pendingEvents.addElement(tn); 622 EventQueue.invokeLater(tn); 623 } 624 } 625 } 626 627 private void notifyRemoved(TestResult what, int index) { 628 if (listenerList.getListenerCount() > 0) { 629 // may want to buffer these messages 630 TableModelEvent e = new TableModelEvent(this, index, index, 631 TableModelEvent.ALL_COLUMNS, 632 TableModelEvent.DELETE); 633 634 if (EventQueue.isDispatchThread()) { 635 // XXX try without this to see perf. impact 636 // dispatch synchronously 637 mod.fireTableChanged(e); 638 } 639 else { 640 // switch event onto AWT event thread 641 TableNotifier tn = new TableNotifier(e, mod); 642 pendingEvents.addElement(tn); 643 EventQueue.invokeLater(tn); 644 } 645 } 646 } 647 648 private void notifyDone() { 649 if (listenerList.getListenerCount() > 0) { 650 // may want to buffer these messages 651 TableModelEvent e = new TableModelEvent(this); 652 // switch event onto AWT event thread 653 TableNotifier tn = new TableNotifier(e, mod); 654 pendingEvents.addElement(tn); 655 EventQueue.invokeLater(tn); 656 } 657 } 658 659 private String[] colNames; 660 661 // must sync. on vLock anytime you access inQueue or liveData 662 private final Object vLock = new Object(); // lock for inQueue & rmQueue 663 private Vector<TestResult> inQueue; // queue of items to be added to live data 664 private Vector<TestResult> rmQueue; // queue of items to be removed from live data 665 private LinkedList<TestResult> liveData; // to allow manual synchronization 666 Vector<TableNotifier> pendingEvents = new Vector<>(); 667 668 volatile boolean isUpdateScheduled; // are updates waiting in inQueue or rmQueue 669 670 private static final int BATCH_SIZE = 100; 671 private static final int COLUMN_COUNT = 2; 672 } 673 674 private class CacheObserver extends TT_NodeCache.TT_NodeCacheObserver { 675 CacheObserver() { 676 super(); 677 // configure our interest list 678 interestList[MSGS_FILTERED] = true; 679 } 680 681 public void testAdded(int msgType, TestResultTable.TreeNode[] path, 682 TestResult what, int index) { 683 synchronized(BP_FilteredOutSubpanel.this) { 684 mod.addTest(what, false); 685 } 686 } 687 688 public void testRemoved(int msgType, TestResultTable.TreeNode[] path, 689 TestResult what, int index) { 690 synchronized(BP_FilteredOutSubpanel.this) { 691 mod.removeTest(what); 692 } 693 } 694 695 public void statsUpdated(int[] stats) { 696 // ignore 697 } 698 } 699 700 /** 701 * This is a double duty class; it commits changes the model and also dispatches 702 * given events. If instance var. lt is null, it dispatches events, otherwise it 703 * triggers a commit on the list thread data using (<tt>goLive()</tt>). 704 * This class is critical because it is the task which gets scheduled on 705 * the event thread. 706 */ 707 class TableNotifier implements Runnable { 708 TableNotifier(TT_BasicNode n, TestTableModel m) { 709 node = n; 710 tm = m; 711 tm.isUpdateScheduled = true; 712 } 713 714 TableNotifier(TableModelEvent e, TestTableModel m) { 715 tm = m; 716 tme = e; 717 } 718 719 public void run() { 720 tm.pendingEvents.remove(this); 721 722 // this message has been cancelled 723 if (!isValid) 724 return; 725 726 if (tme == null) { 727 // consume the update event 728 tm.isUpdateScheduled = false; 729 tm.goLive(); 730 } 731 else { 732 tm.fireTableChanged(tme); 733 } 734 } 735 736 public void cancel() { 737 isValid = false; 738 } 739 740 // used to validate the event at dispatch time 741 TT_BasicNode node; 742 743 // go live data, no event dispatch 744 TestTableModel tm; 745 746 // event dispatch items 747 private TableModelEvent tme; 748 private boolean isValid = true; 749 } // list notifier 750 751 /** 752 * One of these listeners is associated with each of the test lists. 753 */ 754 class InputListener extends MouseAdapter implements ListSelectionListener, ActionListener { 755 // ActionListener 756 public void actionPerformed(ActionEvent e) { 757 if (e.getActionCommand().equals("action.cpnamelist") || 758 e.getActionCommand().equals("action.cpnamestr")) { 759 final int[] rows = table.getSelectedRows(); 760 761 if (rows.length > 0) { 762 String[] result = new String[rows.length]; 763 for (int i = 0; i < rows.length; i++) { 764 if (table.getValueAt(rows[i], 0) instanceof TestResult) { 765 TestResult r = (TestResult) table.getValueAt( 766 rows[i], 0); 767 result[i] = r.getTestName(); 768 } else 769 // should not happen 770 result[i] = table.getValueAt(rows[i], 0).toString(); 771 } // for 772 773 StringSelection payload = null; 774 if (e.getActionCommand().equals("action.cpnamestr")) { 775 payload = new StringSelection(StringArray.join(result)); 776 } else { 777 payload = new StringSelection(StringArray.join(result, "\n")); 778 } 779 780 // send to clipboard 781 if (payload != null) { 782 Toolkit.getDefaultToolkit().getSystemClipboard(). 783 setContents(payload, null); 784 Clipboard selection = Toolkit.getDefaultToolkit().getSystemSelection(); 785 if (selection != null) 786 selection.setContents(payload, null); 787 } 788 789 } else { // now rows selected 790 // XXX show error dialog? 791 } 792 } // Copy 793 table.repaint(); 794 795 } // action performed 796 797 // Mouse Adapter 798 public void mouseClicked(MouseEvent e) { 799 if (e.getComponent() == table) { 800 if (e.getButton() == MouseEvent.BUTTON3) { 801 popupTable.show(e.getComponent(), e.getX(), e.getY()); 802 } else { 803 JTable tbl = (JTable)(e.getComponent()); 804 int col = table.columnAtPoint(e.getPoint()); 805 int row = table.rowAtPoint(e.getPoint()); 806 TableModel tm = table.getModel(); 807 808 // an empty table can't do anything 809 if (tm.getRowCount() < 1) { 810 // clear the message field 811 showMessage(""); 812 return; 813 } 814 815 // always use col 1, which is where the TestResult is 816 // we only really care which row was clicked on 817 TestResult tr = (TestResult)(tm.getValueAt(row, 0)); 818 819 if (e.getClickCount() == 1) { 820 // show vital stats only 821 showMessage(I18NUtils.getStatusMessage(tr.getStatus())); 822 } 823 else if (e.getClickCount() == 2) { 824 // construct the path required by the model 825 TestResultTable.TreeNode[] path = TestResultTable.getObjectPath(tr); 826 827 // sanity check, could happen in exceptional cases (out of memory) 828 if (path == null || path.length == 0) 829 return; 830 831 Object[] fp = new Object[path.length + 1]; 832 System.arraycopy(path, 0, fp, 0, path.length); 833 fp[fp.length-1] = tr; 834 835 model.showTest(tr, fp); 836 } 837 } 838 } 839 } 840 841 // ListSelectionListener 842 public void valueChanged(ListSelectionEvent e) { 843 int index = e.getLastIndex(); 844 845 if (mod.getRowCount() == 0 || index >= mod.getRowCount()) { 846 // swing seems to generate re-selection events when the 847 // list model is 'replaced' with a new one (completely 848 // invalidated). for some reason it changes the selection 849 // index without doing bounds checking 850 return; // ignore invalid event 851 } 852 853 if (index != lastIndex) { 854 TestResult tr = 855 (TestResult)(mod.getValueAt(index, 0)); 856 857 // show vital stats only 858 showMessage(I18NUtils.getStatusMessage(tr.getStatus())); 859 lastIndex = index; 860 } 861 } 862 863 private int lastIndex = -2; 864 } 865 866 /** 867 * Action to handle user pressing enter to select a test. 868 */ 869 private class KbTableAction extends AbstractAction { 870 KbTableAction(I18NResourceBundle bund, String key) { 871 desc = bund.getString(key + ".desc"); 872 name = bund.getString(key + ".act"); 873 } 874 875 public void actionPerformed(ActionEvent e) { 876 int row = table.getSelectedRow(); 877 878 // nothing selected, ignore event 879 if (row < 0) 880 return; 881 882 Object target = table.getModel().getValueAt(row, 0); 883 884 // this shouldn't be the case... 885 if (!(target instanceof TestResult)) 886 return; 887 888 TestResult tr = (TestResult)target; 889 TestResultTable.TreeNode[] path = TestResultTable.getObjectPath(tr); 890 891 // sanity check, could happen in exceptional cases (out of memory) 892 if (path == null || path.length == 0) 893 return; 894 895 Object[] fp = new Object[path.length + 1]; 896 System.arraycopy(path, 0, fp, 0, path.length); 897 fp[fp.length-1] = tr; 898 899 model.showTest(tr, fp); 900 } 901 902 public Object getValue(String key) { 903 if (key == null) 904 throw new NullPointerException(); 905 906 if (key.equals(NAME)) 907 return name; 908 else if (key.equals(SHORT_DESCRIPTION)) 909 return desc; 910 else 911 return null; 912 } 913 914 private String name; 915 private String desc; 916 } 917 918 class TestCellRenderer extends DefaultTableCellRenderer { 919 public TestCellRenderer(UIFactory uif) { 920 setOpaque(false); 921 } 922 923 public Component getTableCellRendererComponent(JTable table, Object value, 924 boolean isSelected, boolean hasFocus, int row, 925 int column) { 926 if (value == null) // very strange... 927 return this; 928 929 if (value instanceof TestResult) { 930 TestResult tr = (TestResult)value; 931 setText(tr.getTestName()); 932 setToolTipText(I18NUtils.getStatusMessage(tr.getStatus())); 933 934 } else if (value instanceof TestFilter) { 935 TestFilter tf = (TestFilter)value; 936 setText(tf.getReason()); 937 setToolTipText(tf.getDescription()); 938 } 939 else { // this will run for the reason column (1) 940 setText(value.toString()); 941 } 942 943 setBorder(spacerBorder); 944 setFont(getFont().deriveFont(Font.PLAIN)); 945 946 if (isSelected) { 947 setOpaque(true); 948 //setBackground(MetalLookAndFeel.getTextHighlightColor()); 949 setBackground(table.getSelectionBackground()); 950 setForeground(table.getSelectionForeground()); 951 } 952 else { 953 //setForeground(MetalLookAndFeel.getPrimaryControlDarkShadow()); 954 setForeground(table.getForeground()); 955 setOpaque(false); 956 } 957 958 // would like to find a better way to do this 959 // seems like we can't do this properly until the first item 960 // is rendered though 961 if (!rowHeightSet) { 962 table.setRowHeight(getFontMetrics(getFont()).getHeight() + 963 ROW_HEIGHT_PADDING); 964 rowHeightSet = true; 965 } 966 967 return this; 968 } 969 970 // border to pad left and right 971 private Border spacerBorder = BorderFactory.createEmptyBorder(3,3,3,3); 972 } 973 974 975 private JTable table; 976 private TestTableModel mod; // JTable model 977 private TT_NodeCache cache; 978 private TT_BasicNode lastNode; // last node updated against 979 private CacheObserver cacheWatcher; 980 private volatile TableSynchronizer resyncThread; 981 private TableCellRenderer renderer; 982 private InputListener listener; 983 private JTextArea infoTa; 984 private boolean rowHeightSet; 985 private static final int ROW_HEIGHT_PADDING = 3; 986 987 private JPopupMenu popupTable; 988 989 private boolean debug = Debug.getBoolean(BP_FilteredOutSubpanel.class); 990 }