/* * $Id$ * * Copyright (c) 2004, 2011, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.javatest.exec; import java.awt.Component; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.LinkedList; import java.util.Vector; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextArea; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.border.Border; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TableModelEvent; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import com.sun.javatest.tool.jthelp.ContextHelpManager; import com.sun.javatest.TestFilter; import com.sun.javatest.TestResult; import com.sun.javatest.TestResultTable; import com.sun.javatest.tool.I18NUtils; import com.sun.javatest.tool.UIFactory; import com.sun.javatest.util.Debug; import com.sun.javatest.util.I18NResourceBundle; import com.sun.javatest.util.StringArray; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import javax.swing.JPopupMenu; /** * This panel renders information about the tests which are "filtered out" in * the current node. It shows the test name, as well as the reason that the * test was filtered out. The tooltip are also upgraded beyond the standard * list to provide more filter information. *

* The background thread has a * two-stage commit process so that the iterator can run at full speed, * ignoring the MT-unsafe swing list model. The changes are reflected in the * real list when the AWT event queue schedules the notification thread which * is created during the iteration. This class also processes the list * click events and usually dispatches changes to the branch panel model. * *

* If you need to synchronize against both the vLock (for live data) and this * class' lock, then blocks should be synchronized against this outer class, * then the vLock. The ordering is vital to avoiding deadlocks. */ class BP_FilteredOutSubpanel extends BP_BranchSubpanel { BP_FilteredOutSubpanel(UIFactory uif, BP_Model bpm, TestTreeModel ttm) { super("fo", uif, bpm, ttm, "br.fo"); init(); ContextHelpManager.setHelpIDString(this, "browse.filteredOutTab.csh"); cacheWatcher = new CacheObserver(); } /** * Clear the table contents and prepare to receive new data. */ void reset(TT_NodeCache cache) { synchronized (BP_FilteredOutSubpanel.this) { if (this.cache != null) this.cache.removeObserver(cacheWatcher); this.cache = cache; if (resyncThread != null) { resyncThread.halt(); } if (mod != null) mod.reset(); } // sync validateEnableState(); //table.clearSelection(); } protected void invalidateFilters() { super.invalidateFilters(); // if we didn't have one, we certainly don't need to disconnect, // and probably don't need to get a new one... if (cache != null) { cache.removeObserver(cacheWatcher); } if (subpanelNode != null) { cache = ttm.getNodeInfo(subpanelNode.getTableNode(), false); validateEnableState(); } updateInfoText(); } /** * Only called when this panel is onscreen and needs to be kept up to date. */ protected synchronized void updateSubpanel(TT_BasicNode currNode) { super.updateSubpanel(currNode); // only run if we change nodes if (lastNode != currNode || filtersInvalidated) { if (debug) Debug.println("updating FO table"); if (resyncThread != null) { resyncThread.halt(); } resyncThread = new TableSynchronizer(); resyncThread.start(); lastNode = currNode; filtersInvalidated = false; validateEnableState(); } } private void updateInfoText() { if (infoTa == null) return; TestFilter f = model.getFilter(); if (f != null) infoTa.setText(uif.getI18NString("br.fo.info.txt", f.getName())); else infoTa.setText(uif.getI18NString("br.fo.noFn.txt")); } /** * Enable or disable this panel as necessary. */ private void validateEnableState() { if (cache.getRejectCount() > 0) { model.setEnabled(BP_FilteredOutSubpanel.this, true); } else if (cache.getRejectCount() == 0) { model.setEnabled(BP_FilteredOutSubpanel.this, false); } else { } } /** * A special thread to repopulate the test lists. */ private class TableSynchronizer extends Thread { TableSynchronizer() { super("filtered-out list synchronizer"); setPriority(Thread.MIN_PRIORITY + 2); } public void run() { // grab cache lock first because many other threads may alter that // object, causing a deadlock here. // sync. to hold observer traffic until re-sync is done synchronized (cache) { synchronized(BP_FilteredOutSubpanel.this) { // resync with this node cache Vector[] newData = cache.addObserver(cacheWatcher, true); // add tests into the list model - this doesn't make the data // live though for (int j = 0; j < newData[newData.length-1].size() - 1; j++) { if (stopping) break; mod.addTest(newData[newData.length-1].elementAt(j), true); } // for if (newData[newData.length-1].size() > 0 && !stopping) { // final item with a notify mod.addTest(newData[newData.length-1].lastElement(), false); } // to indicate completion resyncThread = null; } // this sync } // cache sync validateEnableState(); } // run() public void halt() { stopping = true; } private volatile boolean stopping; } private void init() { mod = new TestTableModel(uif); renderer = new TestCellRenderer(uif); listener = new InputListener(); table = uif.createTable("br.fo.tbl", mod); table.setOpaque(true); table.setRowSelectionAllowed(true); table.setColumnSelectionAllowed(false); table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.getSelectionModel().addListSelectionListener(listener); // setup col 0 TableColumn tc = table.getColumnModel().getColumn(0); tc.setCellRenderer(renderer); tc.setResizable(true); // setup col 1 tc = table.getColumnModel().getColumn(1); tc.setCellRenderer(renderer); tc.setResizable(true); uif.setAccessibleInfo(this, "br.fo"); setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTH; gbc.gridy = 0; gbc.ipady = 12; gbc.weightx = 1.0; gbc.weighty = 0.0; infoTa = uif.createMessageArea("br.fo.info"); infoTa.setOpaque(false); add(infoTa, gbc); gbc.gridy = 1; gbc.weightx = 2.0; gbc.weighty = 1.0; gbc.fill = GridBagConstraints.BOTH; add(uif.createScrollPane(table, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED), gbc); updateInfoText(); InputListener tableListener = new InputListener(); table.addMouseListener(tableListener); table.getSelectionModel().addListSelectionListener(tableListener); // to trigger test selection when enter is pressed table.getInputMap(JComponent.WHEN_FOCUSED).put( KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "gotoTest"); table.getActionMap().put("gotoTest", new KbTableAction(uif.getI18NResourceBundle(), "br.list.enter")); String[] actions = { }; popupTable = uif.createPopupMenu("br", actions, (ActionListener)tableListener); actions = new String[] { "action.cpnamelist", "action.cpnamestr" }; popupTable.add(uif.createMenu("br.cp", actions, (ActionListener)tableListener)); // this is necessary to make sure that the split pane can resize // this panel. without setting the min., the panel seems to take // all it is given, and never gives it back. setMinimumSize(new Dimension(150,100)); } /** * It is assumed that this will run on the event thread. private void setEmpty(boolean state) { if (state && list.getModel() != EmptyListModel.getInstance()) { list.setModel(EmptyListModel.getInstance()); model.setEnabled(BP_TestListSubpanel.this, false); lastMsg = ""; } else if (!state && list.getModel() == EmptyListModel.getInstance()) { list.setModel(mod); model.setEnabled(BP_TestListSubpanel.this, true); } } */ // ------------- inner class ------------- /** * Enumerates tree in background to populate the list. * If this thread is running, consider list data incomplete. * Swing cannot handle an updating model, so there is a two-stage absorbtion * of data. This thread runs with no delay reading the TRT and placing that data * into an "offline" queue. It periodically schedules an event on the GUI event * thread; when that thread run, it copies the data from the offline area to the * online area, which is what the ListModel presents. This workaround assumes that * Swing will never dispatch more than one event at a time. */ private class TestTableModel extends AbstractTableModel { TestTableModel(UIFactory uif) { super(); colNames = new String[] { uif.getI18NString("br.fo.col0.txt"), uif.getI18NString("br.fo.col1.txt") }; if (debug) { Debug.println("TableModel constructed: "); Debug.println(" -> " + this); } init(); } // ---------- TableModel interface ---------- public int getRowCount() { synchronized (liveData) { return liveData.size(); } } public int getColumnCount() { return COLUMN_COUNT; } public String getColumnName(int columnIndex) { if (columnIndex >= colNames.length) throw new IndexOutOfBoundsException(); else return colNames[columnIndex]; } public Object getValueAt(int row, int column) { if (column == 0) { synchronized (liveData) { return liveData.get(row); } } else if (column == 1) { synchronized (liveData) { Object tst = (liveData.get(row)); Object r = null; if (cache != null && r == null) { r = cache.getRejectReason((TestResult)tst); } if (r == null) r = uif.getI18NString("br.fo.noFi.txt"); return r; } } else throw new IndexOutOfBoundsException( "Index into filtered out table is out of range: " + row + ", " + column); } public boolean isCellEditable(int rowIndex, int colIndex) { return false; } // ---------- Custom methods for this model ---------- /** * @param suppressNotify Actively request that no update be scheduled. */ void addTest(TestResult tr, boolean suppressNotify) { synchronized (vLock) { // make sure this item is not already in the list if (!inQueue.contains(tr)) { inQueue.addElement(tr); } } // sync // try not to saturate the GUI event thread if (!suppressNotify && !isUpdateScheduled) { TableNotifier tn = new TableNotifier(subpanelNode, this); pendingEvents.addElement(tn); EventQueue.invokeLater(tn); } } /** * Remove the given test from the list. * Ignored if the test is not in the list. */ void removeTest(TestResult tr) { synchronized (vLock) { rmQueue.addElement(tr); // try not to saturate the GUI event thread if (!isUpdateScheduled) { TableNotifier tn = new TableNotifier(subpanelNode, this); pendingEvents.addElement(tn); EventQueue.invokeLater(tn); } } // sync } void reset() { synchronized (vLock) { init(); } // force GUI to update the now empty list notifyDone(); } // ------------ private -------------- private void init() { // discard all pending events // this is necessary to ensure that update events which haven't // been processed are not processed after the model has changed // arguably, this should be solved by putting this init() onto // the event thread synchronized (pendingEvents) { for (int i = 0; i < pendingEvents.size(); i++) { TableNotifier tn = (TableNotifier)(pendingEvents.get(i)); tn.cancel(); } // for } inQueue = new Vector<>(); rmQueue = new Vector<>(); liveData = new LinkedList<>(); isUpdateScheduled = false; } /** * Transfer data from the internal queue to the live data queue. * This is part of the Swing threading workaround. This method immediately * exits if there is no work to do. It also dispatches model update events * if necessary. * This method always runs on the event dispatch thread. */ private void goLive() { int firstNew, lastNew = 0; if (debug) Debug.println("BP_TL.TLM - goLive() starting."); // this is sync. against the outer class because we may change the // list model object during execution of this block synchronized (BP_FilteredOutSubpanel.this) { synchronized (vLock) { if (inQueue.size() == 0 && rmQueue.size() == 0) { if (debug) Debug.println("BP_TT.TTM - goLive() nothing to do, returning"); return; } processRemoveQueue(); //preprocessAddQueue(); // now add the new items if (inQueue.size() != 0) { synchronized (liveData) { firstNew = liveData.size(); if (inQueue.size() < BATCH_SIZE) { liveData.addAll(inQueue); lastNew = liveData.size()-1; inQueue.setSize(0); } else { // only add some of the new items for (int i = 0; i < BATCH_SIZE; i++) { liveData.add(inQueue.remove(0)); } // for // schedule a future update if (!isUpdateScheduled) { TableNotifier tn = new TableNotifier( subpanelNode, this); pendingEvents.addElement(tn); EventQueue.invokeLater(tn); } lastNew = liveData.size()-1; } } // sync // dispatch update range event to Swing if (listenerList.getListenerCount() > 0) { TableModelEvent e = new TableModelEvent(this, firstNew, lastNew, TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT); TableNotifier tn = new TableNotifier(e, mod); pendingEvents.addElement(tn); EventQueue.invokeLater(tn); } } // enable this tab now that it has data /* if (liveData.size() > 0) { // switch back from an empty list setEmpty(false); } else { setEmpty(true); } */ // this clears the "please wait" message if needed if (table.getSelectedRow() == -1 && inQueue.size() == 0) showMessage(""); } // sync } if (debug) Debug.println("BP_TL.LT - goLive() finished"); } /** * Remove tests in the removal queue from the live data or the incoming data. * vLock should be locked when you call this method */ private void processRemoveQueue() { if (rmQueue.size() == 0) return; while (rmQueue.size() > 0) { TestResult target = rmQueue.remove(0); int targetIndex = liveData.indexOf(target); if (targetIndex != -1) { synchronized (liveData) { // necessary for proper synchronization // should not be a problem really, based on how other // locking is done, all work on liveData occurs in goLive() targetIndex = liveData.indexOf(target); // only should happen if the item disappears if (targetIndex == -1) continue; liveData.remove(targetIndex); // WARNING: since we are continually changing the contents of // the data, you must notify the observers synchronously to get // proper results notifyRemoved(target, targetIndex); } // sync } } // while } /** * Remove duplicates in the add queue. * vLock should be locked when you call this method */ private void preprocessAddQueue() { // make sure this list does not contain dups for (int i = 0; i < inQueue.size(); i++) { if (liveData.contains(inQueue.elementAt(i))) { inQueue.remove(i); i--; } else { } } // for } // --------- event utility methods ----------- /** * Notify observers that the given index was added */ private void notifyAdded(TestResult what, int index) { if (listenerList.getListenerCount() > 0) { // may want to buffer these messages for performance TableModelEvent e = new TableModelEvent (this, index, index, TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT); if (EventQueue.isDispatchThread()) { // XXX try without this to see perf. impact // dispatch synchronously mod.fireTableChanged(e); } else { // switch event onto AWT event thread TableNotifier tn = new TableNotifier(e, mod); pendingEvents.addElement(tn); EventQueue.invokeLater(tn); } } } private void notifyRemoved(TestResult what, int index) { if (listenerList.getListenerCount() > 0) { // may want to buffer these messages TableModelEvent e = new TableModelEvent(this, index, index, TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE); if (EventQueue.isDispatchThread()) { // XXX try without this to see perf. impact // dispatch synchronously mod.fireTableChanged(e); } else { // switch event onto AWT event thread TableNotifier tn = new TableNotifier(e, mod); pendingEvents.addElement(tn); EventQueue.invokeLater(tn); } } } private void notifyDone() { if (listenerList.getListenerCount() > 0) { // may want to buffer these messages TableModelEvent e = new TableModelEvent(this); // switch event onto AWT event thread TableNotifier tn = new TableNotifier(e, mod); pendingEvents.addElement(tn); EventQueue.invokeLater(tn); } } private String[] colNames; // must sync. on vLock anytime you access inQueue or liveData private final Object vLock = new Object(); // lock for inQueue & rmQueue private Vector inQueue; // queue of items to be added to live data private Vector rmQueue; // queue of items to be removed from live data private LinkedList liveData; // to allow manual synchronization Vector pendingEvents = new Vector<>(); volatile boolean isUpdateScheduled; // are updates waiting in inQueue or rmQueue private static final int BATCH_SIZE = 100; private static final int COLUMN_COUNT = 2; } private class CacheObserver extends TT_NodeCache.TT_NodeCacheObserver { CacheObserver() { super(); // configure our interest list interestList[MSGS_FILTERED] = true; } public void testAdded(int msgType, TestResultTable.TreeNode[] path, TestResult what, int index) { synchronized(BP_FilteredOutSubpanel.this) { mod.addTest(what, false); } } public void testRemoved(int msgType, TestResultTable.TreeNode[] path, TestResult what, int index) { synchronized(BP_FilteredOutSubpanel.this) { mod.removeTest(what); } } public void statsUpdated(int[] stats) { // ignore } } /** * This is a double duty class; it commits changes the model and also dispatches * given events. If instance var. lt is null, it dispatches events, otherwise it * triggers a commit on the list thread data using (goLive()). * This class is critical because it is the task which gets scheduled on * the event thread. */ class TableNotifier implements Runnable { TableNotifier(TT_BasicNode n, TestTableModel m) { node = n; tm = m; tm.isUpdateScheduled = true; } TableNotifier(TableModelEvent e, TestTableModel m) { tm = m; tme = e; } public void run() { tm.pendingEvents.remove(this); // this message has been cancelled if (!isValid) return; if (tme == null) { // consume the update event tm.isUpdateScheduled = false; tm.goLive(); } else { tm.fireTableChanged(tme); } } public void cancel() { isValid = false; } // used to validate the event at dispatch time TT_BasicNode node; // go live data, no event dispatch TestTableModel tm; // event dispatch items private TableModelEvent tme; private boolean isValid = true; } // list notifier /** * One of these listeners is associated with each of the test lists. */ class InputListener extends MouseAdapter implements ListSelectionListener, ActionListener { // ActionListener public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("action.cpnamelist") || e.getActionCommand().equals("action.cpnamestr")) { final int[] rows = table.getSelectedRows(); if (rows.length > 0) { String[] result = new String[rows.length]; for (int i = 0; i < rows.length; i++) { if (table.getValueAt(rows[i], 0) instanceof TestResult) { TestResult r = (TestResult) table.getValueAt( rows[i], 0); result[i] = r.getTestName(); } else // should not happen result[i] = table.getValueAt(rows[i], 0).toString(); } // for StringSelection payload = null; if (e.getActionCommand().equals("action.cpnamestr")) { payload = new StringSelection(StringArray.join(result)); } else { payload = new StringSelection(StringArray.join(result, "\n")); } // send to clipboard if (payload != null) { Toolkit.getDefaultToolkit().getSystemClipboard(). setContents(payload, null); Clipboard selection = Toolkit.getDefaultToolkit().getSystemSelection(); if (selection != null) selection.setContents(payload, null); } } else { // now rows selected // XXX show error dialog? } } // Copy table.repaint(); } // action performed // Mouse Adapter public void mouseClicked(MouseEvent e) { if (e.getComponent() == table) { if (e.getButton() == MouseEvent.BUTTON3) { popupTable.show(e.getComponent(), e.getX(), e.getY()); } else { JTable tbl = (JTable)(e.getComponent()); int col = table.columnAtPoint(e.getPoint()); int row = table.rowAtPoint(e.getPoint()); TableModel tm = table.getModel(); // an empty table can't do anything if (tm.getRowCount() < 1) { // clear the message field showMessage(""); return; } // always use col 1, which is where the TestResult is // we only really care which row was clicked on TestResult tr = (TestResult)(tm.getValueAt(row, 0)); if (e.getClickCount() == 1) { // show vital stats only showMessage(I18NUtils.getStatusMessage(tr.getStatus())); } else if (e.getClickCount() == 2) { // construct the path required by the model TestResultTable.TreeNode[] path = TestResultTable.getObjectPath(tr); // sanity check, could happen in exceptional cases (out of memory) if (path == null || path.length == 0) return; Object[] fp = new Object[path.length + 1]; System.arraycopy(path, 0, fp, 0, path.length); fp[fp.length-1] = tr; model.showTest(tr, fp); } } } } // ListSelectionListener public void valueChanged(ListSelectionEvent e) { int index = e.getLastIndex(); if (mod.getRowCount() == 0 || index >= mod.getRowCount()) { // swing seems to generate re-selection events when the // list model is 'replaced' with a new one (completely // invalidated). for some reason it changes the selection // index without doing bounds checking return; // ignore invalid event } if (index != lastIndex) { TestResult tr = (TestResult)(mod.getValueAt(index, 0)); // show vital stats only showMessage(I18NUtils.getStatusMessage(tr.getStatus())); lastIndex = index; } } private int lastIndex = -2; } /** * Action to handle user pressing enter to select a test. */ private class KbTableAction extends AbstractAction { KbTableAction(I18NResourceBundle bund, String key) { desc = bund.getString(key + ".desc"); name = bund.getString(key + ".act"); } public void actionPerformed(ActionEvent e) { int row = table.getSelectedRow(); // nothing selected, ignore event if (row < 0) return; Object target = table.getModel().getValueAt(row, 0); // this shouldn't be the case... if (!(target instanceof TestResult)) return; TestResult tr = (TestResult)target; TestResultTable.TreeNode[] path = TestResultTable.getObjectPath(tr); // sanity check, could happen in exceptional cases (out of memory) if (path == null || path.length == 0) return; Object[] fp = new Object[path.length + 1]; System.arraycopy(path, 0, fp, 0, path.length); fp[fp.length-1] = tr; model.showTest(tr, fp); } public Object getValue(String key) { if (key == null) throw new NullPointerException(); if (key.equals(NAME)) return name; else if (key.equals(SHORT_DESCRIPTION)) return desc; else return null; } private String name; private String desc; } class TestCellRenderer extends DefaultTableCellRenderer { public TestCellRenderer(UIFactory uif) { setOpaque(false); } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (value == null) // very strange... return this; if (value instanceof TestResult) { TestResult tr = (TestResult)value; setText(tr.getTestName()); setToolTipText(I18NUtils.getStatusMessage(tr.getStatus())); } else if (value instanceof TestFilter) { TestFilter tf = (TestFilter)value; setText(tf.getReason()); setToolTipText(tf.getDescription()); } else { // this will run for the reason column (1) setText(value.toString()); } setBorder(spacerBorder); setFont(getFont().deriveFont(Font.PLAIN)); if (isSelected) { setOpaque(true); //setBackground(MetalLookAndFeel.getTextHighlightColor()); setBackground(table.getSelectionBackground()); setForeground(table.getSelectionForeground()); } else { //setForeground(MetalLookAndFeel.getPrimaryControlDarkShadow()); setForeground(table.getForeground()); setOpaque(false); } // would like to find a better way to do this // seems like we can't do this properly until the first item // is rendered though if (!rowHeightSet) { table.setRowHeight(getFontMetrics(getFont()).getHeight() + ROW_HEIGHT_PADDING); rowHeightSet = true; } return this; } // border to pad left and right private Border spacerBorder = BorderFactory.createEmptyBorder(3,3,3,3); } private JTable table; private TestTableModel mod; // JTable model private TT_NodeCache cache; private TT_BasicNode lastNode; // last node updated against private CacheObserver cacheWatcher; private volatile TableSynchronizer resyncThread; private TableCellRenderer renderer; private InputListener listener; private JTextArea infoTa; private boolean rowHeightSet; private static final int ROW_HEIGHT_PADDING = 3; private JPopupMenu popupTable; private boolean debug = Debug.getBoolean(BP_FilteredOutSubpanel.class); }