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 }