View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.log4j.chainsaw;
19  
20  import java.awt.BorderLayout;
21  import java.awt.Color;
22  import java.awt.Component;
23  import java.awt.Container;
24  import java.awt.Dimension;
25  import java.awt.EventQueue;
26  import java.awt.Font;
27  import java.awt.FontMetrics;
28  import java.awt.Graphics;
29  import java.awt.Point;
30  import java.awt.Toolkit;
31  import java.awt.Window;
32  import java.awt.datatransfer.Clipboard;
33  import java.awt.datatransfer.StringSelection;
34  import java.awt.event.ActionEvent;
35  import java.awt.event.ActionListener;
36  import java.awt.event.FocusEvent;
37  import java.awt.event.FocusListener;
38  import java.awt.event.InputEvent;
39  import java.awt.event.KeyEvent;
40  import java.awt.event.KeyListener;
41  import java.awt.event.MouseAdapter;
42  import java.awt.event.MouseEvent;
43  import java.awt.event.MouseListener;
44  import java.awt.event.MouseMotionAdapter;
45  import java.awt.event.WindowAdapter;
46  import java.awt.event.WindowEvent;
47  import java.beans.PropertyChangeEvent;
48  import java.beans.PropertyChangeListener;
49  import java.io.EOFException;
50  import java.io.File;
51  import java.io.FileReader;
52  import java.io.FileWriter;
53  import java.io.IOException;
54  import java.io.ObjectInputStream;
55  import java.io.ObjectOutputStream;
56  import java.io.StringReader;
57  import java.io.UnsupportedEncodingException;
58  import java.net.URLEncoder;
59  import java.text.DateFormat;
60  import java.text.NumberFormat;
61  import java.text.SimpleDateFormat;
62  import java.util.ArrayList;
63  import java.util.Date;
64  import java.util.Enumeration;
65  import java.util.EventObject;
66  import java.util.HashMap;
67  import java.util.HashSet;
68  import java.util.Iterator;
69  import java.util.List;
70  import java.util.Locale;
71  import java.util.Map;
72  import java.util.Set;
73  import java.util.StringTokenizer;
74  import java.util.Vector;
75  
76  import javax.swing.AbstractAction;
77  import javax.swing.AbstractListModel;
78  import javax.swing.Action;
79  import javax.swing.BorderFactory;
80  import javax.swing.Box;
81  import javax.swing.BoxLayout;
82  import javax.swing.ButtonGroup;
83  import javax.swing.ComboBoxEditor;
84  import javax.swing.ImageIcon;
85  import javax.swing.JButton;
86  import javax.swing.JCheckBoxMenuItem;
87  import javax.swing.JColorChooser;
88  import javax.swing.JComboBox;
89  import javax.swing.JComponent;
90  import javax.swing.JDialog;
91  import javax.swing.JEditorPane;
92  import javax.swing.JFrame;
93  import javax.swing.JLabel;
94  import javax.swing.JMenuItem;
95  import javax.swing.JPanel;
96  import javax.swing.JPopupMenu;
97  import javax.swing.JRadioButtonMenuItem;
98  import javax.swing.JScrollPane;
99  import javax.swing.JSeparator;
100 import javax.swing.JSplitPane;
101 import javax.swing.JTable;
102 import javax.swing.JTextField;
103 import javax.swing.JToolBar;
104 import javax.swing.KeyStroke;
105 import javax.swing.ListSelectionModel;
106 import javax.swing.MutableComboBoxModel;
107 import javax.swing.SwingConstants;
108 import javax.swing.SwingUtilities;
109 import javax.swing.UIManager;
110 import javax.swing.WindowConstants;
111 import javax.swing.event.CellEditorListener;
112 import javax.swing.event.ChangeEvent;
113 import javax.swing.event.DocumentEvent;
114 import javax.swing.event.DocumentListener;
115 import javax.swing.event.ListSelectionEvent;
116 import javax.swing.event.ListSelectionListener;
117 import javax.swing.event.PopupMenuEvent;
118 import javax.swing.event.PopupMenuListener;
119 import javax.swing.event.TableColumnModelEvent;
120 import javax.swing.event.TableColumnModelListener;
121 import javax.swing.event.TableModelEvent;
122 import javax.swing.event.TableModelListener;
123 import javax.swing.table.TableCellEditor;
124 import javax.swing.table.TableColumn;
125 import javax.swing.table.TableColumnModel;
126 import javax.swing.text.Document;
127 
128 import org.apache.log4j.Level;
129 import org.apache.log4j.LogManager;
130 import org.apache.log4j.Logger;
131 import org.apache.log4j.PatternLayout;
132 import org.apache.log4j.chainsaw.color.ColorPanel;
133 import org.apache.log4j.chainsaw.color.RuleColorizer;
134 import org.apache.log4j.chainsaw.filter.FilterModel;
135 import org.apache.log4j.chainsaw.helper.SwingHelper;
136 import org.apache.log4j.chainsaw.icons.ChainsawIcons;
137 import org.apache.log4j.chainsaw.icons.LineIconFactory;
138 import org.apache.log4j.chainsaw.layout.DefaultLayoutFactory;
139 import org.apache.log4j.chainsaw.layout.EventDetailLayout;
140 import org.apache.log4j.chainsaw.layout.LayoutEditorPane;
141 import org.apache.log4j.chainsaw.messages.MessageCenter;
142 import org.apache.log4j.chainsaw.prefs.LoadSettingsEvent;
143 import org.apache.log4j.chainsaw.prefs.Profileable;
144 import org.apache.log4j.chainsaw.prefs.SaveSettingsEvent;
145 import org.apache.log4j.chainsaw.prefs.SettingsManager;
146 import org.apache.log4j.chainsaw.xstream.TableColumnConverter;
147 import org.apache.log4j.helpers.Constants;
148 import org.apache.log4j.rule.ColorRule;
149 import org.apache.log4j.rule.ExpressionRule;
150 import org.apache.log4j.rule.Rule;
151 import org.apache.log4j.spi.LoggingEvent;
152 import org.apache.log4j.spi.LoggingEventFieldResolver;
153 
154 import com.thoughtworks.xstream.XStream;
155 import com.thoughtworks.xstream.io.xml.DomDriver;
156 
157 
158 /**
159  * A LogPanel provides a view to a collection of LoggingEvents.<br>
160  * <br>
161  * As events are received, the keywords in the 'tab identifier' application
162  * preference  are replaced with the values from the received event.  The
163  * main application uses  this expression to route received LoggingEvents to
164  * individual LogPanels which  match each event's resolved expression.<br>
165  * <br>
166  * The LogPanel's capabilities can be broken up into four areas:<br>
167  * <ul><li> toolbar - provides 'find' and 'refine focus' features
168  * <li> logger tree - displays a tree of the logger hierarchy, which can be used
169  * to filter the display
170  * <li> table - displays the events which pass the filtering rules
171  * <li>detail panel - displays information about the currently selected event
172  * </ul>
173  * Here is a complete list of LogPanel's capabilities:<br>
174  * <ul><li>display selected LoggingEvent row number and total LoggingEvent count
175  * <li>pause or unpause reception of LoggingEvents
176  * <li>configure, load and save column settings (displayed columns, order, width)
177  * <li>configure, load and save color rules
178  * filter displayed LoggingEvents based on the logger tree settings
179  * <li>filter displayed LoggingEvents based on a 'refine focus' expression
180  * (evaluates only those LoggingEvents which pass the logger tree filter
181  * <li>colorize LoggingEvents based on expressions
182  * <li>hide, show and configure the detail pane and tooltip
183  * <li>configure the formatting of the logger, level and timestamp fields
184  * <li>dock or undock
185  * <li>table displays first line of exception, but when cell is clicked, a
186  * popup opens to display the full stack trace
187  * <li>find
188  * <li>scroll to bottom
189  * <li>sort
190  * <li>provide a context menu which can be used to build color or display expressions
191  * <li>hide or show the logger tree
192  * <li>toggle the container storing the LoggingEvents to use either a
193  * CyclicBuffer (defaults to max size of 5000,  but configurable  through
194  * CHAINSAW_CAPACITY system property) or ArrayList (no max size)
195  * <li>use the mouse context menu to 'best-fit' columns, define display
196  * expression filters based on mouse location and access other capabilities
197  *</ul>
198  *
199  *@see org.apache.log4j.chainsaw.color.ColorPanel
200  *@see org.apache.log4j.rule.ExpressionRule
201  *@see org.apache.log4j.spi.LoggingEventFieldResolver
202  *
203  *@author Scott Deboy (sdeboy at apache.org)
204  *@author Paul Smith (psmith at apache.org)
205  *@author Stephen Pain
206  *@author Isuru Suriarachchi
207  *
208  */
209 public class LogPanel extends DockablePanel implements EventBatchListener, Profileable {
210   private static final DateFormat TIMESTAMP_DATE_FORMAT = new SimpleDateFormat(Constants.TIMESTAMP_RULE_FORMAT);
211   private static final double DEFAULT_DETAIL_SPLIT_LOCATION = 0.71d;
212   private static final double DEFAULT_LOG_TREE_SPLIT_LOCATION = 0.2d;
213   private final String identifier;
214   private final ChainsawStatusBar statusBar;
215   private final JFrame logPanelPreferencesFrame = new JFrame();
216   private ColorPanel colorPanel;
217   private final JFrame colorFrame = new JFrame();
218   private final JFrame undockedFrame;
219   private final DockablePanel externalPanel;
220   private final Action dockingAction;
221   private final JToolBar undockedToolbar;
222   private final JSortTable table;
223   private final TableColorizingRenderer renderer;
224   private final EventContainer tableModel;
225   private final JEditorPane detail;
226   private final JSplitPane lowerPanel;
227   private final DetailPaneUpdater detailPaneUpdater;
228   private final JPanel detailPanel = new JPanel(new BorderLayout());
229   private final JSplitPane nameTreeAndMainPanelSplit;
230   private final LoggerNameTreePanel logTreePanel;
231   private final LogPanelPreferenceModel preferenceModel = new LogPanelPreferenceModel();
232   private ApplicationPreferenceModel applicationPreferenceModel;
233   private final LogPanelPreferencePanel logPanelPreferencesPanel;
234   private final FilterModel filterModel = new FilterModel();
235   private final RuleColorizer colorizer = new RuleColorizer();
236   private final RuleMediator tableRuleMediator = new RuleMediator(false);
237   private final RuleMediator searchRuleMediator = new RuleMediator(true);
238   private final EventDetailLayout detailLayout = new EventDetailLayout();
239   private double lastLogTreePanelSplitLocation = DEFAULT_LOG_TREE_SPLIT_LOCATION;
240   private Point currentPoint;
241   private JTable currentTable;
242   private boolean paused = false;
243   private Rule findRule;
244   private String currentFindRuleText;
245   private Rule findMarkerRule;
246   private final int dividerSize;
247   static final String TABLE_COLUMN_ORDER = "table.columns.order";
248   static final String TABLE_COLUMN_WIDTHS = "table.columns.widths";
249   static final String COLORS_EXTENSION = ".colors";
250   private static final int LOG_PANEL_SERIALIZATION_VERSION_NUMBER = 2; //increment when format changes
251   private int previousLastIndex = -1;
252   private final Logger logger = LogManager.getLogger(LogPanel.class);
253   private AutoFilterComboBox filterCombo;
254   private AutoFilterComboBox findCombo;
255   private JScrollPane eventsPane;
256   private int currentSearchMatchCount;
257   private Rule clearTableExpressionRule;
258   private int lowerPanelDividerLocation;
259   private EventContainer searchModel;
260   private final JSortTable searchTable;
261   private TableColorizingRenderer searchRenderer;
262   private ToggleToolTips mainToggleToolTips;
263   private ToggleToolTips searchToggleToolTips;
264   private JScrollPane detailPane;
265   private JScrollPane searchPane;
266   //only one tableCellEditor, shared by both tables
267   private TableCellEditor markerCellEditor;
268   private JToolBar detailToolbar;
269   private boolean searchResultsDisplayed;
270   private ColorizedEventAndSearchMatchThumbnail colorizedEventAndSearchMatchThumbnail;
271   private EventTimeDeltaMatchThumbnail eventTimeDeltaMatchThumbnail;
272   private boolean isDetailPanelVisible;
273 
274   /**
275    * Creates a new LogPanel object.  If a LogPanel with this identifier has
276    * been loaded previously, reload settings saved on last exit.
277    *
278    * @param statusBar shared status bar, provided by main application
279    * @param identifier used to load and save settings
280    */
281   public LogPanel(final ChainsawStatusBar statusBar, final String identifier, int cyclicBufferSize,
282                   Map allColorizers, final ApplicationPreferenceModel applicationPreferenceModel) {
283     this.identifier = identifier;
284     this.statusBar = statusBar;
285     this.applicationPreferenceModel = applicationPreferenceModel;
286     this.logPanelPreferencesPanel = new LogPanelPreferencePanel(preferenceModel, applicationPreferenceModel);
287     logger.debug("creating logpanel for " + identifier);
288 
289     setLayout(new BorderLayout());
290 
291     String prototypeValue = "1231231231231231231231";
292 
293     filterCombo = new AutoFilterComboBox();
294     findCombo = new AutoFilterComboBox();
295 
296     filterCombo.setPrototypeDisplayValue(prototypeValue);
297     buildCombo(filterCombo, true, findCombo.model);
298 
299     findCombo.setPrototypeDisplayValue(prototypeValue);
300     buildCombo(findCombo, false, filterCombo.model);
301 
302     final Map columnNameKeywordMap = new HashMap();
303     columnNameKeywordMap.put(ChainsawConstants.CLASS_COL_NAME, LoggingEventFieldResolver.CLASS_FIELD);
304     columnNameKeywordMap.put(ChainsawConstants.FILE_COL_NAME, LoggingEventFieldResolver.FILE_FIELD);
305     columnNameKeywordMap.put(ChainsawConstants.LEVEL_COL_NAME, LoggingEventFieldResolver.LEVEL_FIELD);
306     columnNameKeywordMap.put(ChainsawConstants.LINE_COL_NAME, LoggingEventFieldResolver.LINE_FIELD);
307     columnNameKeywordMap.put(ChainsawConstants.LOGGER_COL_NAME, LoggingEventFieldResolver.LOGGER_FIELD);
308     columnNameKeywordMap.put(ChainsawConstants.NDC_COL_NAME, LoggingEventFieldResolver.NDC_FIELD);
309     columnNameKeywordMap.put(ChainsawConstants.MESSAGE_COL_NAME, LoggingEventFieldResolver.MSG_FIELD);
310     columnNameKeywordMap.put(ChainsawConstants.THREAD_COL_NAME, LoggingEventFieldResolver.THREAD_FIELD);
311     columnNameKeywordMap.put(ChainsawConstants.THROWABLE_COL_NAME, LoggingEventFieldResolver.EXCEPTION_FIELD);
312     columnNameKeywordMap.put(ChainsawConstants.TIMESTAMP_COL_NAME, LoggingEventFieldResolver.TIMESTAMP_FIELD);
313     columnNameKeywordMap.put(ChainsawConstants.ID_COL_NAME.toUpperCase(), LoggingEventFieldResolver.PROP_FIELD + Constants.LOG4J_ID_KEY);
314     columnNameKeywordMap.put(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE.toUpperCase(), LoggingEventFieldResolver.PROP_FIELD + ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE);
315     columnNameKeywordMap.put(ChainsawConstants.MILLIS_DELTA_COL_NAME_LOWERCASE.toUpperCase(), LoggingEventFieldResolver.PROP_FIELD + ChainsawConstants.MILLIS_DELTA_COL_NAME_LOWERCASE);
316 
317     logPanelPreferencesFrame.setTitle("'" + identifier + "' Log Panel Preferences");
318     logPanelPreferencesFrame.setIconImage(
319       ((ImageIcon) ChainsawIcons.ICON_PREFERENCES).getImage());
320     logPanelPreferencesFrame.getContentPane().add(new JScrollPane(logPanelPreferencesPanel));
321 
322     logPanelPreferencesFrame.setSize(740, 520);
323 
324     logPanelPreferencesPanel.setOkCancelActionListener(
325       new ActionListener() {
326         public void actionPerformed(ActionEvent e) {
327           logPanelPreferencesFrame.setVisible(false);
328         }
329       });
330 
331         KeyStroke escape = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
332             Action closeLogPanelPreferencesFrameAction = new AbstractAction() {
333                 public void actionPerformed(ActionEvent e) {
334                   logPanelPreferencesFrame.setVisible(false);
335                 }
336             };
337             logPanelPreferencesFrame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escape, "ESCAPE"); logPanelPreferencesFrame.getRootPane().
338                     getActionMap().put("ESCAPE", closeLogPanelPreferencesFrameAction);
339 
340 
341     setDetailPaneConversionPattern(
342       DefaultLayoutFactory.getDefaultPatternLayout());
343       detailLayout.setConversionPattern(
344       DefaultLayoutFactory.getDefaultPatternLayout());
345 
346     undockedFrame = new JFrame(identifier);
347     undockedFrame.setDefaultCloseOperation(
348       WindowConstants.DO_NOTHING_ON_CLOSE);
349 
350     if (ChainsawIcons.UNDOCKED_ICON != null) {
351       undockedFrame.setIconImage(
352         new ImageIcon(ChainsawIcons.UNDOCKED_ICON).getImage());
353     }
354 
355     externalPanel = new DockablePanel();
356     externalPanel.setLayout(new BorderLayout());
357 
358     undockedFrame.addWindowListener(
359       new WindowAdapter() {
360         public void windowClosing(WindowEvent e) {
361           dock();
362         }
363       });
364 
365     undockedToolbar = createDockwindowToolbar();
366     externalPanel.add(undockedToolbar, BorderLayout.NORTH);
367     undockedFrame.getContentPane().add(externalPanel);
368     undockedFrame.setSize(new Dimension(1024, 768));
369     undockedFrame.pack();
370 
371     preferenceModel.addPropertyChangeListener(
372       "scrollToBottom",
373       new PropertyChangeListener() {
374         public void propertyChange(PropertyChangeEvent evt) {
375           boolean value = ((Boolean) evt.getNewValue()).booleanValue();
376           if (value) {
377             scrollToBottom();
378           }
379         }
380       });
381     /*
382      * Menus on which the preferencemodels rely
383      */
384 
385     /**
386      * Setup a popup menu triggered for Timestamp column to allow time stamp
387      * format changes
388      */
389     final JPopupMenu dateFormatChangePopup = new JPopupMenu();
390     final JRadioButtonMenuItem isoButton =
391       new JRadioButtonMenuItem(
392         new AbstractAction("Use ISO8601Format") {
393           public void actionPerformed(ActionEvent e) {
394             preferenceModel.setDateFormatPattern("ISO8601");
395           }
396         });
397     final JRadioButtonMenuItem simpleTimeButton =
398       new JRadioButtonMenuItem(
399         new AbstractAction("Use simple time") {
400           public void actionPerformed(ActionEvent e) {
401             preferenceModel.setDateFormatPattern("HH:mm:ss");
402           }
403         });
404 
405     ButtonGroup dfBG = new ButtonGroup();
406     dfBG.add(isoButton);
407     dfBG.add(simpleTimeButton);
408     simpleTimeButton.setSelected(true);
409     dateFormatChangePopup.add(isoButton);
410     dateFormatChangePopup.add(simpleTimeButton);
411 
412     final JCheckBoxMenuItem menuItemLoggerTree =
413       new JCheckBoxMenuItem("Show Logger Tree");
414     menuItemLoggerTree.addActionListener(
415       new ActionListener() {
416         public void actionPerformed(ActionEvent e) {
417           preferenceModel.setLogTreePanelVisible(
418             menuItemLoggerTree.isSelected());
419         }
420       });
421     menuItemLoggerTree.setIcon(new ImageIcon(ChainsawIcons.WINDOW_ICON));
422 
423     final JCheckBoxMenuItem menuItemToggleDetails =
424       new JCheckBoxMenuItem("Show Detail Pane");
425     menuItemToggleDetails.addActionListener(
426       new ActionListener() {
427         public void actionPerformed(ActionEvent e) {
428           preferenceModel.setDetailPaneVisible(
429             menuItemToggleDetails.isSelected());
430         }
431       });
432 
433     menuItemToggleDetails.setIcon(new ImageIcon(ChainsawIcons.INFO));
434 
435     /*
436      * add preferencemodel listeners
437      */
438     preferenceModel.addPropertyChangeListener("levelIcons",
439       new PropertyChangeListener() {
440         public void propertyChange(PropertyChangeEvent evt) {
441           boolean useIcons = ((Boolean) evt.getNewValue()).booleanValue();
442           renderer.setLevelUseIcons(useIcons);
443           table.tableChanged(new TableModelEvent(tableModel));
444           searchRenderer.setLevelUseIcons(useIcons);
445           searchTable.tableChanged(new TableModelEvent(searchModel));
446         }
447       });
448 
449     /*
450      * add preferencemodel listeners
451      */
452     preferenceModel.addPropertyChangeListener("wrapMessage",
453       new PropertyChangeListener() {
454         public void propertyChange(PropertyChangeEvent evt) {
455           boolean wrap = ((Boolean) evt.getNewValue()).booleanValue();
456           renderer.setWrapMessage(wrap);
457           table.tableChanged(new TableModelEvent(tableModel));
458           searchRenderer.setWrapMessage(wrap);
459           searchTable.tableChanged(new TableModelEvent(searchModel));
460         }
461       });
462 
463     preferenceModel.addPropertyChangeListener("searchResultsVisible",
464       new PropertyChangeListener() {
465         public void propertyChange(PropertyChangeEvent evt) {
466           boolean displaySearchResultsInDetailsIfAvailable = ((Boolean) evt.getNewValue()).booleanValue();
467           if (displaySearchResultsInDetailsIfAvailable) {
468             showSearchResults();
469           } else {
470             hideSearchResults();
471           }
472         }
473       });
474 
475       preferenceModel.addPropertyChangeListener("highlightSearchMatchText",
476         new PropertyChangeListener() {
477           public void propertyChange(PropertyChangeEvent evt) {
478             boolean highlightText = ((Boolean) evt.getNewValue()).booleanValue();
479             renderer.setHighlightSearchMatchText(highlightText);
480             table.tableChanged(new TableModelEvent(tableModel));
481             searchRenderer.setHighlightSearchMatchText(highlightText);
482             searchTable.tableChanged(new TableModelEvent(searchModel));
483           }
484         });
485 
486     preferenceModel.addPropertyChangeListener(
487       "detailPaneVisible",
488       new PropertyChangeListener() {
489         public void propertyChange(PropertyChangeEvent evt) {
490           boolean detailPaneVisible = ((Boolean) evt.getNewValue()).booleanValue();
491 
492           if (detailPaneVisible) {
493             showDetailPane();
494           } else {
495             //don't hide the detail pane if search results are being displayed
496             if (!searchResultsDisplayed) {
497               hideDetailPane();
498             }
499           }
500         }
501       });
502 
503     preferenceModel.addPropertyChangeListener(
504       "logTreePanelVisible",
505       new PropertyChangeListener() {
506         public void propertyChange(PropertyChangeEvent evt) {
507           boolean newValue = ((Boolean) evt.getNewValue()).booleanValue();
508 
509           if (newValue) {
510             showLogTreePanel();
511           } else {
512             hideLogTreePanel();
513           }
514         }
515       });
516     
517     preferenceModel.addPropertyChangeListener("toolTips",
518       new PropertyChangeListener() {
519         public void propertyChange(PropertyChangeEvent evt) {
520           boolean toolTips = ((Boolean) evt.getNewValue()).booleanValue();
521           renderer.setToolTipsVisible(toolTips);
522           searchRenderer.setToolTipsVisible(toolTips);
523         }
524       });
525 
526     preferenceModel.addPropertyChangeListener("visibleColumns",
527       new PropertyChangeListener() {
528     	public void propertyChange(PropertyChangeEvent evt) {
529     		//remove all columns and re-add visible
530             TableColumnModel columnModel = table.getColumnModel();
531             while (columnModel.getColumnCount() > 0) {
532                 columnModel.removeColumn(columnModel.getColumn(0));
533         		}
534             for (Iterator iter = preferenceModel.getVisibleColumnOrder().iterator();iter.hasNext();) {
535               TableColumn c = (TableColumn)iter.next();
536               if (c.getHeaderValue().toString().equalsIgnoreCase(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE))
537               {
538                 c.setCellEditor(markerCellEditor);
539               }
540               columnModel.addColumn(c);
541         		}
542           TableColumnModel searchColumnModel = searchTable.getColumnModel();
543           while (searchColumnModel.getColumnCount() > 0) {
544               searchColumnModel.removeColumn(searchColumnModel.getColumn(0));
545           }
546           for (Iterator iter = preferenceModel.getVisibleColumnOrder().iterator();iter.hasNext();) {
547             TableColumn c = (TableColumn)iter.next();
548             searchColumnModel.addColumn(c);
549           }
550       	}
551       });
552 
553     PropertyChangeListener datePrefsChangeListener =
554       new PropertyChangeListener() {
555         public void propertyChange(PropertyChangeEvent evt) {
556           LogPanelPreferenceModel model = (LogPanelPreferenceModel) evt.getSource();
557 
558           isoButton.setSelected(model.isUseISO8601Format());
559           simpleTimeButton.setSelected(!model.isUseISO8601Format() && !model.isCustomDateFormat());
560 
561           if (model.getTimeZone() != null) {
562             renderer.setTimeZone(model.getTimeZone());
563             searchRenderer.setTimeZone(model.getTimeZone());
564           }
565           
566           if (model.isUseISO8601Format()) {
567             renderer.setDateFormatter(new SimpleDateFormat(Constants.ISO8601_PATTERN));
568             searchRenderer.setDateFormatter(new SimpleDateFormat(Constants.ISO8601_PATTERN));
569           } else {
570       		try {
571               renderer.setDateFormatter(new SimpleDateFormat(model.getDateFormatPattern()));
572           } catch (IllegalArgumentException iae) {
573             model.setDefaultDatePatternFormat();
574             renderer.setDateFormatter(new SimpleDateFormat(Constants.ISO8601_PATTERN));
575           }
576   		    try {
577               searchRenderer.setDateFormatter(new SimpleDateFormat(model.getDateFormatPattern()));
578           } catch (IllegalArgumentException iae) {
579             model.setDefaultDatePatternFormat();
580             searchRenderer.setDateFormatter(new SimpleDateFormat(Constants.ISO8601_PATTERN));
581           }
582         }
583 
584         table.tableChanged(new TableModelEvent(tableModel));
585         searchTable.tableChanged(new TableModelEvent(searchModel));
586         }
587       };
588 
589     preferenceModel.addPropertyChangeListener("dateFormatPattern", datePrefsChangeListener);
590     preferenceModel.addPropertyChangeListener("dateFormatTimeZone", datePrefsChangeListener);
591 
592     preferenceModel.addPropertyChangeListener("clearTableExpression", new PropertyChangeListener() {
593         public void propertyChange(PropertyChangeEvent evt) {
594             LogPanelPreferenceModel model = (LogPanelPreferenceModel)evt.getSource();
595             String expression = model.getClearTableExpression();
596             try {
597                 clearTableExpressionRule = ExpressionRule.getRule(expression);
598                 logger.info("clearTableExpressionRule set to: " + expression);
599             } catch (Exception e) {
600                 logger.info("clearTableExpressionRule invalid - ignoring: " + expression);
601                 clearTableExpressionRule = null;
602             }
603         }
604     });
605 
606     preferenceModel.addPropertyChangeListener("loggerPrecision",
607       new PropertyChangeListener() {
608         public void propertyChange(PropertyChangeEvent evt) {
609           LogPanelPreferenceModel model = (LogPanelPreferenceModel) evt.getSource();
610 
611           renderer.setLoggerPrecision(model.getLoggerPrecision());
612           table.tableChanged(new TableModelEvent(tableModel));
613 
614           searchRenderer.setLoggerPrecision(model.getLoggerPrecision());
615           searchTable.tableChanged(new TableModelEvent(searchModel));
616         }
617       });
618 
619     preferenceModel.addPropertyChangeListener("toolTips",
620       new PropertyChangeListener() {
621         public void propertyChange(PropertyChangeEvent evt) {
622           boolean value = ((Boolean) evt.getNewValue()).booleanValue();
623           searchToggleToolTips.setSelected(value);
624           mainToggleToolTips.setSelected(value);
625         }
626       });
627 
628     preferenceModel.addPropertyChangeListener(
629       "logTreePanelVisible",
630       new PropertyChangeListener() {
631         public void propertyChange(PropertyChangeEvent evt) {
632           boolean value = ((Boolean) evt.getNewValue()).booleanValue();
633           menuItemLoggerTree.setSelected(value);
634         }
635       });
636 
637     preferenceModel.addPropertyChangeListener(
638       "detailPaneVisible",
639       new PropertyChangeListener() {
640         public void propertyChange(PropertyChangeEvent evt) {
641           boolean value = ((Boolean) evt.getNewValue()).booleanValue();
642           menuItemToggleDetails.setSelected(value);
643         }
644       });
645 
646     applicationPreferenceModel.addPropertyChangeListener("searchColor", new PropertyChangeListener() {
647         public void propertyChange(PropertyChangeEvent evt)
648         {
649             if (table != null) {
650               table.repaint();
651             }
652             if (searchTable != null) {
653               searchTable.repaint();
654             }
655         }
656     });
657 
658     applicationPreferenceModel.addPropertyChangeListener("alternatingColor", new PropertyChangeListener() {
659         public void propertyChange(PropertyChangeEvent evt)
660         {
661             if (table != null) {
662               table.repaint();
663             }
664             if (searchTable != null) {
665               searchTable.repaint();
666            }
667         }
668     });
669 
670     /*
671      *End of preferenceModel listeners
672      */
673     tableModel = new ChainsawCyclicBufferTableModel(cyclicBufferSize, colorizer, "main");
674     table = new JSortTable(tableModel);
675 
676     markerCellEditor = new MarkerCellEditor();
677     table.setName("main");
678     table.setColumnSelectionAllowed(false);
679     table.setRowSelectionAllowed(true);
680 
681     searchModel = new ChainsawCyclicBufferTableModel(cyclicBufferSize, colorizer, "search");
682     searchTable = new JSortTable(searchModel);
683 
684     searchTable.setName("search");
685     searchTable.setColumnSelectionAllowed(false);
686     searchTable.setRowSelectionAllowed(true);
687 
688     //we've mapped f2, shift f2 and ctrl-f2 to marker-related actions, unmap them from the table
689     table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("F2"), "none");
690     table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.SHIFT_MASK), "none");
691     table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "none");
692     table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK), "none");
693 
694     //we're also mapping ctrl-a to scroll-to-top, unmap from the table
695     table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_A, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "none");
696         
697     searchTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("F2"), "none");
698     searchTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.SHIFT_MASK), "none");
699     searchTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "none");
700     searchTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | InputEvent.SHIFT_MASK), "none");
701 
702     //we're also mapping ctrl-a to scroll-to-top, unmap from the table
703     searchTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_A, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "none");
704 
705     //add a listener to update the 'refine focus'
706     tableModel.addNewKeyListener(new NewKeyListener() {
707 		public void newKeyAdded(NewKeyEvent e) {
708           columnNameKeywordMap.put(e.getKey(), "PROP." + e.getKey());
709 		}
710     });
711 
712     /*
713      * Set the Display rule to use the mediator, the model will add itself as
714      * a property change listener and update itself when the rule changes.
715      */
716     tableModel.setRuleMediator(tableRuleMediator);
717     searchModel.setRuleMediator(searchRuleMediator);
718 
719     tableModel.addEventCountListener(
720       new EventCountListener() {
721         public void eventCountChanged(int currentCount, int totalCount) {
722           if (LogPanel.this.isVisible()) {
723             statusBar.setSelectedLine(
724               table.getSelectedRow() + 1, currentCount, totalCount, getIdentifier());
725           }
726         }
727       });
728 
729     tableModel.addEventCountListener(
730       new EventCountListener() {
731         final NumberFormat formatter = NumberFormat.getPercentInstance();
732         boolean warning75 = false;
733         boolean warning100 = false;
734 
735         public void eventCountChanged(int currentCount, int totalCount) {
736           if (preferenceModel.isCyclic()) {
737             double percent =
738               ((double) totalCount) / ((ChainsawCyclicBufferTableModel) tableModel)
739               .getMaxSize();
740             String msg = null;
741             boolean wasWarning = warning75 || warning100;
742             if ((percent > 0.75) && (percent < 1.0) && !warning75) {
743               msg =
744                 "Warning :: " + formatter.format(percent) + " of the '"
745                 + getIdentifier() + "' buffer has been used";
746               warning75 = true;
747             } else if ((percent >= 1.0) && !warning100) {
748               msg =
749                 "Warning :: " + formatter.format(percent) + " of the '"
750                 + getIdentifier()
751                 + "' buffer has been used.  Older events are being discarded.";
752               warning100 = true;
753             } else {
754                 //clear msg
755                 msg = "";
756                 warning75 = false;
757                 warning100 = false;
758             }
759 
760             if (msg != null && wasWarning) {
761               MessageCenter.getInstance().getLogger().info(msg);
762             }
763           }
764         }
765       });
766 
767     /*
768      * Logger tree panel
769      *
770      */
771     LogPanelLoggerTreeModel logTreeModel = new LogPanelLoggerTreeModel();
772     logTreePanel = new LoggerNameTreePanel(logTreeModel, preferenceModel, this, colorizer, filterModel);
773     logTreePanel.getLoggerVisibilityRule().addPropertyChangeListener(new PropertyChangeListener()
774     {
775         public void propertyChange(PropertyChangeEvent evt)
776         {
777             if (evt.getPropertyName().equals("searchExpression")) {
778                 findCombo.setSelectedItem(evt.getNewValue().toString());
779                 findNext();
780             }
781         }
782     });
783       
784     tableModel.addLoggerNameListener(logTreeModel);
785     tableModel.addLoggerNameListener(logTreePanel);
786 
787     /**
788      * Set the LoggerRule to be the LoggerTreePanel, as this visual component
789      * is a rule itself, and the RuleMediator will automatically listen when
790      * it's rule state changes.
791      */
792     tableRuleMediator.setLoggerRule(logTreePanel.getLoggerVisibilityRule());
793     searchRuleMediator.setLoggerRule(logTreePanel.getLoggerVisibilityRule());
794 
795     colorizer.setLoggerRule(logTreePanel.getLoggerColorRule());
796 
797     /*
798      * Color rule frame and panel
799      */
800     colorFrame.setTitle("'" + identifier + "' color settings");
801     colorFrame.setIconImage(
802       ((ImageIcon) ChainsawIcons.ICON_PREFERENCES).getImage());
803 
804     allColorizers.put(identifier, colorizer);
805     colorPanel = new ColorPanel(colorizer, filterModel, allColorizers, applicationPreferenceModel);
806 
807     colorFrame.getContentPane().add(colorPanel);
808 
809         Action closeColorPanelAction = new AbstractAction() {
810             public void actionPerformed(ActionEvent e) {
811               colorPanel.hidePanel();
812             }
813         };
814         colorFrame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escape, "ESCAPE"); colorFrame.getRootPane().
815                 getActionMap().put("ESCAPE", closeColorPanelAction);
816 
817     colorPanel.setCloseActionListener(
818       new ActionListener() {
819         public void actionPerformed(ActionEvent e) {
820           colorFrame.setVisible(false);
821         }
822       });
823 
824     colorizer.addPropertyChangeListener(
825       "colorrule",
826       new PropertyChangeListener() {
827         public void propertyChange(PropertyChangeEvent evt) {
828           for (Iterator iter = tableModel.getAllEvents().iterator();iter.hasNext();) {
829             LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper)iter.next();
830             loggingEventWrapper.updateColorRuleColors(colorizer.getBackgroundColor(loggingEventWrapper.getLoggingEvent()), colorizer.getForegroundColor(loggingEventWrapper.getLoggingEvent()));
831           }
832 //          no need to update searchmodel events since tablemodel and searchmodel share all events, and color rules aren't different between the two
833 //          if that changes, un-do the color syncing in loggingeventwrapper & re-enable this code
834 //
835 //          for (Iterator iter = searchModel.getAllEvents().iterator();iter.hasNext();) {
836 //             LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper)iter.next();
837 //             loggingEventWrapper.updateColorRuleColors(colorizer.getBackgroundColor(loggingEventWrapper.getLoggingEvent()), colorizer.getForegroundColor(loggingEventWrapper.getLoggingEvent()));
838 //           }
839           colorizedEventAndSearchMatchThumbnail.configureColors();
840           lowerPanel.revalidate();
841           lowerPanel.repaint();
842 
843           searchTable.revalidate();
844           searchTable.repaint();
845         }
846       });
847 
848     /*
849      * Table definition.  Actual construction is above (next to tablemodel)
850      */
851     table.setRowHeight(ChainsawConstants.DEFAULT_ROW_HEIGHT);
852     table.setRowMargin(0);
853     table.getColumnModel().setColumnMargin(0);
854     table.setShowGrid(false);
855     table.getColumnModel().addColumnModelListener(new ChainsawTableColumnModelListener(table));
856     table.setAutoCreateColumnsFromModel(false);
857     table.addMouseMotionListener(new TableColumnDetailMouseListener(table, tableModel));
858     table.addMouseListener(new TableMarkerListener(table, tableModel, searchModel));
859     table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
860 
861     searchTable.setRowHeight(ChainsawConstants.DEFAULT_ROW_HEIGHT);
862     searchTable.setRowMargin(0);
863     searchTable.getColumnModel().setColumnMargin(0);
864     searchTable.setShowGrid(false);
865     searchTable.getColumnModel().addColumnModelListener(new ChainsawTableColumnModelListener(searchTable));
866     searchTable.setAutoCreateColumnsFromModel(false);
867     searchTable.addMouseMotionListener(new TableColumnDetailMouseListener(searchTable, searchModel));
868     searchTable.addMouseListener(new TableMarkerListener(searchTable, searchModel, tableModel));
869     searchTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
870 
871 
872     //set valueisadjusting if holding down a key - don't process setdetail events
873     table.addKeyListener(
874       new KeyListener() {
875         public void keyTyped(KeyEvent e) {
876         }
877 
878         public void keyPressed(KeyEvent e) {
879           synchronized (detail) {
880             table.getSelectionModel().setValueIsAdjusting(true);
881             detail.notify();
882           }
883         }
884 
885         public void keyReleased(KeyEvent e) {
886           synchronized (detail) {
887             table.getSelectionModel().setValueIsAdjusting(false);
888             detail.notify();
889           }
890         }
891       });
892 
893     table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
894     searchTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
895 
896     table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
897         public void valueChanged(ListSelectionEvent evt) {
898             if (((evt.getFirstIndex() == evt.getLastIndex())
899                 && (evt.getFirstIndex() > 0) && previousLastIndex != -1) || (evt.getValueIsAdjusting())) {
900               return;
901             }
902             boolean lastIndexOnLastRow = (evt.getLastIndex() == (table.getRowCount() - 1));
903             boolean lastIndexSame = (previousLastIndex == evt.getLastIndex());
904 
905             /*
906              * when scroll-to-bottom is active, here is what events look like:
907              * rowcount-1: 227, last: 227, previous last: 191..first: 191
908              *
909              * when the user has unselected the bottom row, here is what the events look like:
910              * rowcount-1: 227, last: 227, previous last: 227..first: 222
911              *
912              * note: previouslast is set after it is evaluated in the bypass scroll check
913             */
914            //System.out.println("rowcount: " + (table.getRowCount() - 1) + ", last: " + evt.getLastIndex() +", previous last: " + previousLastIndex + "..first: " + evt.getFirstIndex() + ", isadjusting: " + evt.getValueIsAdjusting());
915 
916             boolean disableScrollToBottom = (lastIndexOnLastRow && lastIndexSame && previousLastIndex != evt.getFirstIndex());
917             if (disableScrollToBottom && isScrollToBottom() && table.getRowCount() > 0) {
918               preferenceModel.setScrollToBottom(false);
919             }
920             previousLastIndex = evt.getLastIndex();
921           }
922         }
923     );
924 
925     table.getSelectionModel().addListSelectionListener(
926       new ListSelectionListener() {
927         public void valueChanged(ListSelectionEvent evt) {
928           if (((evt.getFirstIndex() == evt.getLastIndex())
929               && (evt.getFirstIndex() > 0) && previousLastIndex != -1) || (evt.getValueIsAdjusting())) {
930             return;
931           }
932 
933           final ListSelectionModel lsm = (ListSelectionModel) evt.getSource();
934 
935           if (lsm.isSelectionEmpty()) {
936             if (isVisible()) {
937               statusBar.setNothingSelected();
938             }
939 
940             if (detail.getDocument().getDefaultRootElement() != null) {
941               detailPaneUpdater.setSelectedRow(-1);
942             }
943           } else {
944             if (table.getSelectedRow() > -1) {
945               int selectedRow = table.getSelectedRow();
946 
947               if (isVisible()) {
948                 updateStatusBar();
949               }
950 
951               try {
952                 if (tableModel.getRowCount() >= selectedRow) {
953                   detailPaneUpdater.setSelectedRow(table.getSelectedRow());
954                 } else {
955                   detailPaneUpdater.setSelectedRow(-1);
956                 }
957               } catch (Exception e) {
958                 e.printStackTrace();
959                 detailPaneUpdater.setSelectedRow(-1);
960               }
961             }
962           }
963         }
964       });
965 
966     renderer = new TableColorizingRenderer(colorizer, applicationPreferenceModel, tableModel, preferenceModel, true);
967     renderer.setToolTipsVisible(preferenceModel.isToolTips());
968 
969     table.setDefaultRenderer(Object.class, renderer);
970 
971     searchRenderer = new TableColorizingRenderer(colorizer, applicationPreferenceModel, searchModel, preferenceModel, false);
972     searchRenderer.setToolTipsVisible(preferenceModel.isToolTips());
973 
974     searchTable.setDefaultRenderer(Object.class, searchRenderer);
975 
976     /*
977      * Throwable popup
978      */
979     table.addMouseListener(new ThrowableDisplayMouseAdapter(table, tableModel));
980     searchTable.addMouseListener(new ThrowableDisplayMouseAdapter(searchTable, searchModel));
981 
982     //select a row in the main table when a row in the search table is selected
983     searchTable.addMouseListener(new MouseAdapter() {
984       public void mouseClicked(MouseEvent e) {
985         LoggingEventWrapper loggingEventWrapper = searchModel.getRow(searchTable.getSelectedRow());
986         if (loggingEventWrapper != null) {
987           int id = new Integer(loggingEventWrapper.getLoggingEvent().getProperty("log4jid")).intValue();
988           //preserve the table's viewble column
989           setSelectedEvent(id);
990         }
991       }
992     });
993 
994     /*
995      * We listen for new Key's coming in so we can get them automatically
996      * added as columns
997      */
998     tableModel.addNewKeyListener(
999       new NewKeyListener() {
1000         public void newKeyAdded(final NewKeyEvent e) {
1001         	SwingHelper.invokeOnEDT(new Runnable() {
1002         		public void run() {
1003            // don't add the column if we already know about it, this could be if we've seen it before and saved the column preferences
1004             //this may throw an illegalargexception - ignore it because we need to add only if not already added
1005         	//if the column is already added, don't add again
1006         	
1007         	try {
1008         	if(table.getColumn(e.getKey())!=null){
1009                 return;
1010             }
1011             //no need to check search table - we use the same columns
1012         	} catch (IllegalArgumentException iae) {}
1013           TableColumn col = new TableColumn(e.getNewModelIndex());
1014           col.setHeaderValue(e.getKey());
1015 
1016           if (preferenceModel.addColumn(col)) {
1017             if (preferenceModel.isColumnVisible(col) || !applicationPreferenceModel.isDefaultColumnsSet() || applicationPreferenceModel.isDefaultColumnsSet() &&
1018                 applicationPreferenceModel.getDefaultColumnNames().contains(col.getHeaderValue())) {
1019               table.addColumn(col);
1020                 searchTable.addColumn(col);
1021           	  preferenceModel.setColumnVisible(e.getKey().toString(), true);
1022             }
1023           }
1024         		}
1025         	});
1026         }
1027       });
1028 
1029     //if the table is refiltered, try to reselect the last selected row
1030     //refilter with a newValue of TRUE means refiltering is about to begin
1031     //refilter with a newValue of FALSE means refiltering is complete
1032     //assuming notification is called on the EDT so we can in the current EDT call update the scroll & selection
1033     tableModel.addPropertyChangeListener("refilter", new PropertyChangeListener() {
1034         private LoggingEventWrapper currentEvent;
1035         public void propertyChange(PropertyChangeEvent evt) {
1036             //if new value is true, filtering is about to begin
1037             //if new value is false, filtering is complete
1038             if (evt.getNewValue().equals(Boolean.TRUE)) {
1039                 int currentRow = table.getSelectedRow();
1040                 if (currentRow > -1) {
1041                     currentEvent = tableModel.getRow(currentRow);
1042                 }
1043             } else {
1044                 if (currentEvent != null) {
1045                     table.scrollToRow(tableModel.getRowIndex(currentEvent));
1046                 }
1047             }
1048         }
1049     });
1050 
1051     table.getTableHeader().addMouseListener(
1052       new MouseAdapter() {
1053         public void mouseClicked(MouseEvent e) {
1054           checkEvent(e);
1055         }
1056 
1057         public void mousePressed(MouseEvent e) {
1058           checkEvent(e);
1059         }
1060 
1061         public void mouseReleased(MouseEvent e) {
1062           checkEvent(e);
1063         }
1064 
1065         private void checkEvent(MouseEvent e) {
1066           if (e.isPopupTrigger()) {
1067             TableColumnModel colModel = table.getColumnModel();
1068             int index = colModel.getColumnIndexAtX(e.getX());
1069             int modelIndex = colModel.getColumn(index).getModelIndex();
1070 
1071             if ((modelIndex + 1) == ChainsawColumns.INDEX_TIMESTAMP_COL_NAME) {
1072               dateFormatChangePopup.show(e.getComponent(), e.getX(), e.getY());
1073             }
1074           }
1075         }
1076       });
1077 
1078     /*
1079      * Upper panel definition
1080      */
1081     JPanel upperPanel = new JPanel();
1082     upperPanel.setLayout(new BoxLayout(upperPanel, BoxLayout.X_AXIS));
1083     upperPanel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 0));
1084 
1085     final JLabel filterLabel = new JLabel("Refine focus on: ");
1086     filterLabel.setFont(filterLabel.getFont().deriveFont(Font.BOLD));
1087 
1088     upperPanel.add(filterLabel);
1089     upperPanel.add(Box.createHorizontalStrut(3));
1090     upperPanel.add(filterCombo);
1091     upperPanel.add(Box.createHorizontalStrut(3));
1092 
1093     final JTextField filterText =(JTextField) filterCombo.getEditor().getEditorComponent();
1094     final JTextField findText =(JTextField) findCombo.getEditor().getEditorComponent();
1095 
1096 
1097     //Adding a button to clear filter expressions which are currently remembered by Chainsaw...
1098     final JButton removeFilterButton = new JButton(" Remove ");
1099 
1100     removeFilterButton.setToolTipText("Click here to remove the selected expression from the list");
1101     removeFilterButton.addActionListener(
1102             new AbstractAction() {
1103                 public void actionPerformed(ActionEvent e){
1104                 	Object selectedItem = filterCombo.getSelectedItem();
1105                     if (e.getSource() == removeFilterButton && selectedItem != null && !selectedItem.toString().trim().equals("")){
1106                       //don't just remove the entry from the store, clear the field
1107                       int index = filterCombo.getSelectedIndex();
1108                       filterText.setText(null);
1109                       filterCombo.setSelectedIndex(-1);
1110                       filterCombo.removeItemAt(index);
1111                       if (!(findCombo.getSelectedItem() != null && findCombo.getSelectedItem().equals(selectedItem))) {
1112                         //now remove the entry from the other model
1113                         ((AutoFilterComboBox.AutoFilterComboBoxModel)findCombo.getModel()).removeElement(selectedItem);
1114                       }
1115                     }
1116                 }
1117             }
1118     );
1119     upperPanel.add(removeFilterButton);
1120     //add some space between refine focus and search sections of the panel
1121     upperPanel.add(Box.createHorizontalStrut(25));
1122 
1123     final JLabel findLabel = new JLabel("Find: ");
1124     findLabel.setFont(filterLabel.getFont().deriveFont(Font.BOLD));
1125 
1126     upperPanel.add(findLabel);
1127     upperPanel.add(Box.createHorizontalStrut(3));
1128 
1129     upperPanel.add(findCombo);
1130     upperPanel.add(Box.createHorizontalStrut(3));
1131 
1132     Action findNextAction = getFindNextAction();
1133     Action findPreviousAction = getFindPreviousAction();
1134     //add up & down search
1135     JButton findNextButton = new SmallButton(findNextAction);
1136     findNextButton.setText("");
1137     findNextButton.getActionMap().put(
1138       findNextAction.getValue(Action.NAME), findNextAction);
1139     findNextButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
1140       (KeyStroke) findNextAction.getValue(Action.ACCELERATOR_KEY),
1141       findNextAction.getValue(Action.NAME));
1142 
1143     JButton findPreviousButton = new SmallButton(findPreviousAction);
1144     findPreviousButton.setText("");
1145     findPreviousButton.getActionMap().put(
1146       findPreviousAction.getValue(Action.NAME), findPreviousAction);
1147     findPreviousButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
1148       (KeyStroke) findPreviousAction.getValue(Action.ACCELERATOR_KEY),
1149       findPreviousAction.getValue(Action.NAME));
1150 
1151     upperPanel.add(findNextButton);
1152 
1153     upperPanel.add(findPreviousButton);
1154     upperPanel.add(Box.createHorizontalStrut(3));
1155     
1156     //Adding a button to clear filter expressions which are currently remembered by Chainsaw...
1157     final JButton removeFindButton = new JButton(" Remove ");
1158     removeFindButton.setToolTipText("Click here to remove the selected expression from the list");
1159     removeFindButton.addActionListener(
1160             new AbstractAction() {
1161                 public void actionPerformed(ActionEvent e){
1162                 	Object selectedItem = findCombo.getSelectedItem();
1163                     if (e.getSource() == removeFindButton && selectedItem != null && !selectedItem.toString().trim().equals("")){
1164                       //don't just remove the entry from the store, clear the field
1165                       int index = findCombo.getSelectedIndex();
1166                       findText.setText(null);
1167                       findCombo.setSelectedIndex(-1);
1168                       findCombo.removeItemAt(index);
1169                       if (!(filterCombo.getSelectedItem() != null && filterCombo.getSelectedItem().equals(selectedItem))) {
1170                         //now remove the entry from the other model if it wasn't selected
1171                         ((AutoFilterComboBox.AutoFilterComboBoxModel)filterCombo.getModel()).removeElement(selectedItem);
1172                       }
1173                     }
1174                 }
1175             }
1176     );
1177     upperPanel.add(removeFindButton);
1178 
1179     //define search and refine focus selection and clear actions
1180     Action findFocusAction = new AbstractAction() {
1181       public void actionPerformed(ActionEvent actionEvent) {
1182         findCombo.requestFocus();
1183       }
1184     };
1185 
1186     Action filterFocusAction = new AbstractAction() {
1187       public void actionPerformed(ActionEvent actionEvent) {
1188         filterCombo.requestFocus();
1189       }
1190     };
1191 
1192     Action findClearAction = new AbstractAction() {
1193       public void actionPerformed(ActionEvent actionEvent) {
1194         findCombo.setSelectedIndex(-1);
1195         findNext();
1196       }
1197     };
1198 
1199     Action filterClearAction = new AbstractAction() {
1200       public void actionPerformed(ActionEvent actionEvent) {
1201         setRefineFocusText("");
1202         filterCombo.refilter();
1203       }
1204     };
1205 
1206     //now add them to the action and input maps for the logpanel
1207         KeyStroke ksFindFocus =
1208       KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
1209     KeyStroke ksFilterFocus =
1210       KeyStroke.getKeyStroke(KeyEvent.VK_R, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
1211         KeyStroke ksFindClear =
1212       KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.SHIFT_MASK |Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
1213     KeyStroke ksFilterClear =
1214       KeyStroke.getKeyStroke(KeyEvent.VK_R,  InputEvent.SHIFT_MASK | Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
1215 
1216     getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ksFindFocus, "FindFocus");
1217     getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ksFilterFocus, "FilterFocus");
1218     getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ksFindClear, "FindClear");
1219     getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ksFilterClear, "FilterClear");
1220 
1221     getActionMap().put("FindFocus", findFocusAction);
1222     getActionMap().put("FilterFocus", filterFocusAction);
1223     getActionMap().put("FindClear", findClearAction);
1224     getActionMap().put("FilterClear", filterClearAction);
1225 
1226     /*
1227      * Detail pane definition
1228      */
1229     detail = new JEditorPane(ChainsawConstants.DETAIL_CONTENT_TYPE, "");
1230     detail.setEditable(false);
1231 
1232     detailPaneUpdater = new DetailPaneUpdater();
1233 
1234     //if the panel gets focus, update the detail pane
1235     addFocusListener(new FocusListener() {
1236 
1237         public void focusGained(FocusEvent e) {
1238             detailPaneUpdater.updateDetailPane();
1239         }
1240 
1241         public void focusLost(FocusEvent e) {
1242             
1243         }
1244     });
1245     findMarkerRule = ExpressionRule.getRule("prop." + ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE + " exists");
1246         
1247     tableModel.addTableModelListener(new TableModelListener() {
1248 		public void tableChanged(TableModelEvent e) {
1249             int currentRow = table.getSelectedRow();
1250             if (e.getFirstRow() <= currentRow && e.getLastRow() >= currentRow) {
1251                 //current row has changed - update
1252                 detailPaneUpdater.setAndUpdateSelectedRow(table.getSelectedRow());
1253             }
1254 		}
1255     });
1256     addPropertyChangeListener("detailPaneConversionPattern", detailPaneUpdater);
1257 
1258     searchPane = new JScrollPane(searchTable);
1259     searchPane.getVerticalScrollBar().setUnitIncrement(ChainsawConstants.DEFAULT_ROW_HEIGHT * 2);
1260     searchPane.setPreferredSize(new Dimension(900, 50));
1261 
1262     //default detail panel to contain detail panel - if searchResultsVisible is true, when a search if triggered, update detail pane to contain search results
1263     detailPane = new JScrollPane(detail);
1264     detailPane.setPreferredSize(new Dimension(900, 50));
1265 
1266     detailPanel.add(detailPane, BorderLayout.CENTER);
1267 
1268     JPanel eventsAndStatusPanel = new JPanel(new BorderLayout());
1269 
1270     eventsPane = new JScrollPane(table);
1271     eventsPane.getVerticalScrollBar().setUnitIncrement(ChainsawConstants.DEFAULT_ROW_HEIGHT * 2);
1272 
1273     eventsAndStatusPanel.add(eventsPane, BorderLayout.CENTER);
1274 
1275     Integer scrollBarWidth = (Integer) UIManager.get("ScrollBar.width");
1276 
1277     JPanel rightPanel = new JPanel();
1278     rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS));
1279 
1280     JPanel rightThumbNailPanel = new JPanel();
1281     rightThumbNailPanel.setLayout(new BoxLayout(rightThumbNailPanel, BoxLayout.Y_AXIS));
1282     rightThumbNailPanel.add(Box.createVerticalStrut(scrollBarWidth.intValue()));
1283     colorizedEventAndSearchMatchThumbnail = new ColorizedEventAndSearchMatchThumbnail();
1284     rightThumbNailPanel.add(colorizedEventAndSearchMatchThumbnail);
1285     rightThumbNailPanel.add(Box.createVerticalStrut(scrollBarWidth.intValue()));
1286     rightPanel.add(rightThumbNailPanel);
1287     //set thumbnail width to be a bit narrower than scrollbar width
1288     if (scrollBarWidth != null) {
1289         rightThumbNailPanel.setPreferredSize(new Dimension(scrollBarWidth.intValue() -4, -1));
1290     }
1291     eventsAndStatusPanel.add(rightPanel, BorderLayout.EAST);
1292 
1293     JPanel leftPanel = new JPanel();
1294     leftPanel.setLayout(new BoxLayout(leftPanel, BoxLayout.Y_AXIS));
1295 
1296     JPanel leftThumbNailPanel = new JPanel();
1297     leftThumbNailPanel.setLayout(new BoxLayout(leftThumbNailPanel, BoxLayout.Y_AXIS));
1298     leftThumbNailPanel.add(Box.createVerticalStrut(scrollBarWidth.intValue()));
1299     eventTimeDeltaMatchThumbnail = new EventTimeDeltaMatchThumbnail();
1300     leftThumbNailPanel.add(eventTimeDeltaMatchThumbnail);
1301     leftThumbNailPanel.add(Box.createVerticalStrut(scrollBarWidth.intValue()));
1302     leftPanel.add(leftThumbNailPanel);
1303 
1304     //set thumbnail width to be a bit narrower than scrollbar width
1305     if (scrollBarWidth != null) {
1306         leftThumbNailPanel.setPreferredSize(new Dimension(scrollBarWidth.intValue() -4, -1));
1307     }
1308     eventsAndStatusPanel.add(leftPanel, BorderLayout.WEST);
1309 
1310     final JPanel statusLabelPanel = new JPanel();
1311     statusLabelPanel.setLayout(new BorderLayout());
1312 
1313     statusLabelPanel.add(upperPanel, BorderLayout.CENTER);
1314     eventsAndStatusPanel.add(statusLabelPanel, BorderLayout.NORTH);
1315 
1316     /*
1317      * Detail panel layout editor
1318      */
1319     detailToolbar = new JToolBar(SwingConstants.HORIZONTAL);
1320     detailToolbar.setFloatable(false);
1321 
1322     final LayoutEditorPane layoutEditorPane = new LayoutEditorPane();
1323     final JDialog layoutEditorDialog =
1324       new JDialog((JFrame) null, "Pattern Editor");
1325     layoutEditorDialog.getContentPane().add(layoutEditorPane);
1326     layoutEditorDialog.setSize(640, 480);
1327 
1328     layoutEditorPane.addCancelActionListener(
1329       new ActionListener() {
1330         public void actionPerformed(ActionEvent e) {
1331           layoutEditorDialog.setVisible(false);
1332         }
1333       });
1334 
1335     layoutEditorPane.addOkActionListener(
1336       new ActionListener() {
1337         public void actionPerformed(ActionEvent e) {
1338           setDetailPaneConversionPattern(
1339             layoutEditorPane.getConversionPattern());
1340           layoutEditorDialog.setVisible(false);
1341         }
1342       });
1343 
1344     Action copyToRefineFocusAction = new AbstractAction("Set 'refine focus' field") {
1345         public void actionPerformed(ActionEvent e) {
1346             String selectedText = detail.getSelectedText();
1347             if (selectedText == null || selectedText.equals("")) {
1348                 //no-op empty searches
1349                 return;
1350             }
1351             filterText.setText("msg ~= '" + selectedText + "'");
1352         }
1353     };
1354 
1355     Action copyToSearchAction = new AbstractAction("Find next") {
1356         public void actionPerformed(ActionEvent e) {
1357             String selectedText = detail.getSelectedText();
1358             if (selectedText == null || selectedText.equals("")) {
1359                 //no-op empty searches
1360                 return;
1361             }
1362             findCombo.setSelectedItem("msg ~= '" + selectedText + "'");
1363             findNext();
1364         }
1365     };
1366 
1367     Action editDetailAction =
1368       new AbstractAction(
1369         "Edit...", new ImageIcon(ChainsawIcons.ICON_EDIT_RECEIVER)) {
1370         public void actionPerformed(ActionEvent e) {
1371           layoutEditorPane.setConversionPattern(
1372             getDetailPaneConversionPattern());
1373 
1374           Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
1375           Point p =
1376             new Point(
1377               ((int) ((size.getWidth() / 2)
1378               - (layoutEditorDialog.getSize().getWidth() / 2))),
1379               ((int) ((size.getHeight() / 2)
1380               - (layoutEditorDialog.getSize().getHeight() / 2))));
1381           layoutEditorDialog.setLocation(p);
1382 
1383           layoutEditorDialog.setVisible(true);
1384         }
1385       };
1386 
1387     editDetailAction.putValue(
1388       Action.SHORT_DESCRIPTION,
1389       "opens a Dialog window to Edit the Pattern Layout text");
1390 
1391     final SmallButton editDetailButton = new SmallButton(editDetailAction);
1392     editDetailButton.setText(null);
1393     detailToolbar.add(Box.createHorizontalGlue());
1394     detailToolbar.add(editDetailButton);
1395     detailToolbar.addSeparator();
1396     detailToolbar.add(Box.createHorizontalStrut(5));
1397 
1398     Action closeDetailAction =
1399       new AbstractAction(null, LineIconFactory.createCloseIcon()) {
1400         public void actionPerformed(ActionEvent arg0) {
1401           preferenceModel.setDetailPaneVisible(false);
1402         }
1403       };
1404 
1405     closeDetailAction.putValue(
1406       Action.SHORT_DESCRIPTION, "Hides the Detail Panel");
1407 
1408     SmallButton closeDetailButton = new SmallButton(closeDetailAction);
1409     detailToolbar.add(closeDetailButton);
1410 
1411     detailPanel.add(detailToolbar, BorderLayout.NORTH);
1412 
1413     lowerPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT, eventsAndStatusPanel, detailPanel);
1414 
1415     dividerSize = lowerPanel.getDividerSize();
1416     lowerPanel.setDividerLocation(-1);
1417 
1418     lowerPanel.setResizeWeight(1.0);
1419     lowerPanel.setBorder(null);
1420     lowerPanel.setContinuousLayout(true);
1421 
1422     JPopupMenu editDetailPopupMenu = new JPopupMenu();
1423 
1424     editDetailPopupMenu.add(copyToRefineFocusAction);
1425     editDetailPopupMenu.add(copyToSearchAction);
1426     editDetailPopupMenu.addSeparator();
1427 
1428     editDetailPopupMenu.add(editDetailAction);
1429     editDetailPopupMenu.addSeparator();
1430 
1431     final ButtonGroup layoutGroup = new ButtonGroup();
1432 
1433     JRadioButtonMenuItem defaultLayoutRadio =
1434       new JRadioButtonMenuItem(
1435         new AbstractAction("Set to Default Layout") {
1436           public void actionPerformed(ActionEvent e) {
1437             setDetailPaneConversionPattern(
1438               DefaultLayoutFactory.getDefaultPatternLayout());
1439           }
1440         });
1441 
1442         JRadioButtonMenuItem fullLayoutRadio =
1443           new JRadioButtonMenuItem(
1444             new AbstractAction("Set to Full Layout") {
1445               public void actionPerformed(ActionEvent e) {
1446                 setDetailPaneConversionPattern(
1447                   DefaultLayoutFactory.getFullPatternLayout());
1448               }
1449             });
1450 
1451     editDetailPopupMenu.add(defaultLayoutRadio);
1452     editDetailPopupMenu.add(fullLayoutRadio);
1453 
1454     layoutGroup.add(defaultLayoutRadio);
1455     layoutGroup.add(fullLayoutRadio);
1456     defaultLayoutRadio.setSelected(true);
1457 
1458     JRadioButtonMenuItem tccLayoutRadio =
1459       new JRadioButtonMenuItem(
1460         new AbstractAction("Set to TCCLayout") {
1461           public void actionPerformed(ActionEvent e) {
1462             setDetailPaneConversionPattern(
1463               PatternLayout.TTCC_CONVERSION_PATTERN);
1464           }
1465         });
1466     editDetailPopupMenu.add(tccLayoutRadio);
1467     layoutGroup.add(tccLayoutRadio);
1468 
1469     PopupListener editDetailPopupListener =
1470       new PopupListener(editDetailPopupMenu);
1471     detail.addMouseListener(editDetailPopupListener);
1472 
1473     /*
1474      * Logger tree splitpane definition
1475      */
1476     nameTreeAndMainPanelSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, logTreePanel, lowerPanel);
1477     nameTreeAndMainPanelSplit.setDividerLocation(-1);
1478 
1479     add(nameTreeAndMainPanelSplit, BorderLayout.CENTER);
1480 
1481     if (isLogTreeVisible()) {
1482         showLogTreePanel();
1483     } else {
1484         hideLogTreePanel();
1485     }
1486 
1487     /*
1488      * Other menu items
1489      */
1490     class BestFit extends JMenuItem {
1491       public BestFit() {
1492         super("Best fit column");
1493     addActionListener(
1494       new ActionListener() {
1495         public void actionPerformed(ActionEvent evt) {
1496           if (currentPoint != null) {
1497             int column = currentTable.columnAtPoint(currentPoint);
1498             int maxWidth = getMaxColumnWidth(column);
1499             currentTable.getColumnModel().getColumn(column).setPreferredWidth(
1500               maxWidth);
1501           }
1502         }
1503       });
1504       }
1505     }
1506 
1507     class ColorPanel extends JMenuItem {
1508       public ColorPanel() {
1509         super("Color settings...");
1510         setIcon(ChainsawIcons.ICON_PREFERENCES);
1511   addActionListener(
1512       new ActionListener() {
1513         public void actionPerformed(ActionEvent evt) {
1514           showColorPreferences();
1515         }
1516       });
1517       }
1518     }
1519 
1520     class LogPanelPreferences extends JMenuItem {
1521       public LogPanelPreferences() {
1522         super("Tab Preferences...");
1523         setIcon(ChainsawIcons.ICON_PREFERENCES);
1524     addActionListener(
1525       new ActionListener() {
1526         public void actionPerformed(ActionEvent evt) {
1527           showPreferences();
1528         }
1529       });
1530     }
1531   }
1532 
1533     class FocusOn extends JMenuItem {
1534       public FocusOn() {
1535         super("Set 'refine focus' field to value under pointer");
1536     addActionListener(
1537       new ActionListener() {
1538         public void actionPerformed(ActionEvent evt) {
1539           if (currentPoint != null) {
1540             String operator = "==";
1541             int column = currentTable.columnAtPoint(currentPoint);
1542             int row = currentTable.rowAtPoint(currentPoint);
1543             String colName = currentTable.getColumnName(column).toUpperCase();
1544             String value = getValueOf(row, column);
1545 
1546             if (columnNameKeywordMap.containsKey(colName)) {
1547               filterText.setText(
1548                 columnNameKeywordMap.get(colName).toString() + " " + operator
1549                 + " '" + value + "'");
1550             }
1551           }
1552         }
1553       });
1554       }
1555     }
1556 
1557     class DefineAddCustomFilter extends JMenuItem {
1558       public DefineAddCustomFilter() {
1559         super("Add value under pointer to 'refine focus' field");
1560   addActionListener(
1561       new ActionListener() {
1562         public void actionPerformed(ActionEvent evt) {
1563           if (currentPoint != null) {
1564             String operator = "==";
1565             int column = currentTable.columnAtPoint(currentPoint);
1566             int row = currentTable.rowAtPoint(currentPoint);
1567             String value = getValueOf(row, column);
1568             String colName = currentTable.getColumnName(column).toUpperCase();
1569 
1570             if (columnNameKeywordMap.containsKey(colName)) {
1571               filterText.setText(
1572                 filterText.getText() + " && "
1573                 + columnNameKeywordMap.get(colName).toString() + " "
1574                 + operator + " '" + value + "'");
1575             }
1576 
1577           }
1578         }
1579       });
1580       }
1581     }
1582 
1583         class DefineAddCustomFind extends JMenuItem {
1584       public DefineAddCustomFind() {
1585         super("Add value under pointer to 'find' field");
1586   addActionListener(
1587       new ActionListener() {
1588         public void actionPerformed(ActionEvent evt) {
1589           if (currentPoint != null) {
1590             String operator = "==";
1591             int column = currentTable.columnAtPoint(currentPoint);
1592             int row = currentTable.rowAtPoint(currentPoint);
1593             String value = getValueOf(row, column);
1594             String colName = currentTable.getColumnName(column).toUpperCase();
1595 
1596             if (columnNameKeywordMap.containsKey(colName)) {
1597               findCombo.setSelectedItem(
1598                 findText.getText() + " && "
1599                 + columnNameKeywordMap.get(colName).toString() + " "
1600                 + operator + " '" + value + "'");
1601               findNext();
1602             }
1603           }
1604         }
1605       });
1606       }
1607     }
1608 
1609     class BuildColorRule extends JMenuItem {
1610       public BuildColorRule() {
1611         super("Define color rule for value under pointer");
1612       addActionListener(
1613       new ActionListener() {
1614         public void actionPerformed(ActionEvent evt) {
1615           if (currentPoint != null) {
1616             String operator = "==";
1617             int column = currentTable.columnAtPoint(currentPoint);
1618             int row = currentTable.rowAtPoint(currentPoint);
1619             String colName = currentTable.getColumnName(column).toUpperCase();
1620             String value = getValueOf(row, column);
1621 
1622             if (columnNameKeywordMap.containsKey(colName)) {
1623                 Color c = JColorChooser.showDialog(getRootPane(), "Choose a color", Color.red);
1624                 if (c != null) {
1625                     String expression = columnNameKeywordMap.get(colName).toString() + " " + operator + " '" + value + "'";
1626                     colorizer.addRule(ChainsawConstants.DEFAULT_COLOR_RULE_NAME, new ColorRule(expression,
1627                             ExpressionRule.getRule(expression), c, ChainsawConstants.COLOR_DEFAULT_FOREGROUND));
1628                 }
1629             }
1630           }
1631         }
1632       });
1633       }
1634     }
1635 
1636     final JPopupMenu mainPopup = new JPopupMenu();
1637     final JPopupMenu searchPopup = new JPopupMenu();
1638 
1639     class ClearFocus extends AbstractAction {
1640       public ClearFocus() {
1641         super("Clear 'refine focus' field");
1642       }
1643         public void actionPerformed(ActionEvent e) {
1644           filterText.setText(null);
1645           tableRuleMediator.setFilterRule(null);
1646           searchRuleMediator.setFilterRule(null);
1647         }
1648       }
1649 
1650     class CopySelection extends AbstractAction {
1651       public CopySelection() {
1652         super("Copy selection to clipboard");
1653       }
1654 
1655         public void actionPerformed(ActionEvent e) {
1656           if (currentTable == null) {
1657             return;
1658           }
1659           int start = currentTable.getSelectionModel().getMinSelectionIndex();
1660           int end = currentTable.getSelectionModel().getMaxSelectionIndex();
1661           StringBuffer result = new StringBuffer();
1662           for (int row=start;row<end+1;row++) {
1663             for (int column=0;column<currentTable.getColumnCount();column++) {
1664               result.append(getValueOf(row, column));
1665               if (column != (currentTable.getColumnCount() - 1)) {
1666                 result.append(" - ");
1667               }
1668             }
1669             result.append(System.getProperty("line.separator"));
1670           }
1671           StringSelection selection = new StringSelection(result.toString());
1672           Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1673           clipboard.setContents(selection, null);
1674       }
1675     }
1676 
1677     class CopyField extends AbstractAction {
1678       public CopyField() {
1679         super("Copy value under pointer to clipboard");
1680       }
1681 
1682         public void actionPerformed(ActionEvent e) {
1683           if (currentPoint != null && currentTable != null) {
1684             int column = currentTable.columnAtPoint(currentPoint);
1685             int row = currentTable.rowAtPoint(currentPoint);
1686             String value = getValueOf(row, column);
1687             StringSelection selection = new StringSelection(value);
1688             Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1689             clipboard.setContents(selection, null);
1690         }
1691       }
1692     }
1693     final JMenuItem menuItemToggleDock = new JMenuItem("Undock/dock");
1694 
1695     dockingAction =
1696       new AbstractAction("Undock") {
1697           public void actionPerformed(ActionEvent evt) {
1698             if (isDocked()) {
1699               undock();
1700             } else {
1701               dock();
1702             }
1703           }
1704         };
1705     dockingAction.putValue(
1706       Action.SMALL_ICON, new ImageIcon(ChainsawIcons.UNDOCK));
1707     menuItemToggleDock.setAction(dockingAction);
1708 
1709     /*
1710      * Popup definition
1711      */
1712     mainPopup.add(new FocusOn());
1713     searchPopup.add(new FocusOn());
1714     mainPopup.add(new DefineAddCustomFilter());
1715     searchPopup.add(new DefineAddCustomFilter());
1716     mainPopup.add(new ClearFocus());
1717     searchPopup.add(new ClearFocus());
1718 
1719     mainPopup.add(new JSeparator());
1720     searchPopup.add(new JSeparator());
1721 
1722     class Search extends JMenuItem {
1723       public Search() {
1724         super("Find value under pointer");
1725 
1726     addActionListener(
1727       new ActionListener() {
1728         public void actionPerformed(ActionEvent evt) {
1729           if (currentPoint != null) {
1730             String operator = "==";
1731             int column = currentTable.columnAtPoint(currentPoint);
1732             int row = currentTable.rowAtPoint(currentPoint);
1733             String colName = currentTable.getColumnName(column).toUpperCase();
1734             String value = getValueOf(row, column);
1735             if (columnNameKeywordMap.containsKey(colName)) {
1736               findCombo.setSelectedItem(
1737                 columnNameKeywordMap.get(colName).toString() + " " + operator
1738                 + " '" + value + "'");
1739               findNext();
1740             }
1741           }
1742         }
1743       });
1744       }
1745     }
1746 
1747      class ClearSearch extends AbstractAction {
1748        public ClearSearch() {
1749          super("Clear find field");
1750        }
1751           public void actionPerformed(ActionEvent e) {
1752             findCombo.setSelectedItem(null);
1753             updateFindRule(null);
1754           }
1755         }
1756 
1757     mainPopup.add(new Search());
1758     searchPopup.add(new Search());
1759     mainPopup.add(new DefineAddCustomFind());
1760     searchPopup.add(new DefineAddCustomFind());
1761     mainPopup.add(new ClearSearch());
1762     searchPopup.add(new ClearSearch());
1763 
1764     mainPopup.add(new JSeparator());
1765     searchPopup.add(new JSeparator());
1766 
1767     class DisplayNormalTimes extends JMenuItem {
1768       public DisplayNormalTimes() {
1769         super("Hide relative times");
1770   addActionListener(
1771       new ActionListener() {
1772         public void actionPerformed(ActionEvent e) {
1773           if (currentPoint != null) {
1774             ((TableColorizingRenderer)currentTable.getDefaultRenderer(Object.class)).setUseNormalTimes();
1775             ((ChainsawCyclicBufferTableModel)currentTable.getModel()).reFilter();
1776             setEnabled(true);
1777           }
1778         }
1779     });
1780       }
1781     }
1782 
1783     class DisplayRelativeTimesToRowUnderCursor extends JMenuItem {
1784     public DisplayRelativeTimesToRowUnderCursor() {
1785       super("Show times relative to this event");
1786       addActionListener(
1787       new ActionListener() {
1788         public void actionPerformed(ActionEvent e) {
1789             if (currentPoint != null) {
1790               int row = currentTable.rowAtPoint(currentPoint);
1791               ChainsawCyclicBufferTableModel cyclicBufferTableModel = (ChainsawCyclicBufferTableModel) currentTable.getModel();
1792               LoggingEventWrapper loggingEventWrapper = cyclicBufferTableModel.getRow(row);
1793               if (loggingEventWrapper != null)
1794               {
1795                   ((TableColorizingRenderer)currentTable.getDefaultRenderer(Object.class)).setUseRelativeTimes(loggingEventWrapper.getLoggingEvent().getTimeStamp());
1796                   cyclicBufferTableModel.reFilter();
1797               }
1798               setEnabled(true);
1799             }
1800         }
1801       });
1802     }
1803     }
1804 
1805     class DisplayRelativeTimesToPreviousRow extends JMenuItem {
1806       public DisplayRelativeTimesToPreviousRow() {
1807         super("Show times relative to previous rows");
1808       addActionListener(
1809         new ActionListener() {
1810           public void actionPerformed(ActionEvent e) {
1811               if (currentPoint != null) {
1812                 ((TableColorizingRenderer)currentTable.getDefaultRenderer(Object.class)).setUseRelativeTimesToPreviousRow();
1813                 ((ChainsawCyclicBufferTableModel)currentTable.getModel()).reFilter();
1814                 setEnabled(true);
1815               }
1816           }
1817         });
1818       }
1819     }
1820 
1821     mainPopup.add(new DisplayRelativeTimesToRowUnderCursor());
1822     searchPopup.add(new DisplayRelativeTimesToRowUnderCursor());
1823     mainPopup.add(new DisplayRelativeTimesToPreviousRow());
1824     searchPopup.add(new DisplayRelativeTimesToPreviousRow());
1825     mainPopup.add(new DisplayNormalTimes());
1826     searchPopup.add(new DisplayNormalTimes());
1827     mainPopup.add(new JSeparator());
1828     searchPopup.add(new JSeparator());
1829 
1830     mainPopup.add(new BuildColorRule());
1831     searchPopup.add(new BuildColorRule());
1832     mainPopup.add(new JSeparator());
1833     searchPopup.add(new JSeparator());
1834     mainPopup.add(new CopyField());
1835     mainPopup.add(new CopySelection());
1836     searchPopup.add(new CopyField());
1837     searchPopup.add(new CopySelection());
1838     mainPopup.add(new JSeparator());
1839     searchPopup.add(new JSeparator());
1840 
1841     mainPopup.add(menuItemToggleDetails);
1842     mainPopup.add(menuItemLoggerTree);
1843     mainToggleToolTips = new ToggleToolTips();
1844     searchToggleToolTips = new ToggleToolTips();
1845     mainPopup.add(mainToggleToolTips);
1846     searchPopup.add(searchToggleToolTips);
1847 
1848     mainPopup.add(new JSeparator());
1849 
1850     mainPopup.add(menuItemToggleDock);
1851 
1852     mainPopup.add(new BestFit());
1853     searchPopup.add(new BestFit());
1854 
1855     mainPopup.add(new JSeparator());
1856 
1857     mainPopup.add(new ColorPanel());
1858     searchPopup.add(new ColorPanel());
1859     mainPopup.add(new LogPanelPreferences());
1860     searchPopup.add(new LogPanelPreferences());
1861 
1862     final PopupListener mainTablePopupListener = new PopupListener(mainPopup);
1863     eventsPane.addMouseListener(mainTablePopupListener);
1864     table.addMouseListener(mainTablePopupListener);
1865 
1866     table.addMouseListener(new MouseListener(){
1867       public void mouseClicked(MouseEvent mouseEvent) {
1868         checkMultiSelect(mouseEvent);
1869       }
1870 
1871       public void mousePressed(MouseEvent mouseEvent) {
1872         checkMultiSelect(mouseEvent);
1873       }
1874 
1875       public void mouseReleased(MouseEvent mouseEvent) {
1876         checkMultiSelect(mouseEvent);
1877       }
1878 
1879       public void mouseEntered(MouseEvent mouseEvent) {
1880         checkMultiSelect(mouseEvent);
1881       }
1882 
1883       public void mouseExited(MouseEvent mouseEvent) {
1884         checkMultiSelect(mouseEvent);
1885       }
1886 
1887       private void checkMultiSelect(MouseEvent mouseEvent) {
1888         if (mouseEvent.isAltDown()) {
1889           table.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
1890         } else {
1891           table.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
1892         }
1893       }
1894     });
1895 
1896 
1897     searchTable.addMouseListener(new MouseListener(){
1898       public void mouseClicked(MouseEvent mouseEvent) {
1899         checkMultiSelect(mouseEvent);
1900       }
1901 
1902       public void mousePressed(MouseEvent mouseEvent) {
1903         checkMultiSelect(mouseEvent);
1904       }
1905 
1906       public void mouseReleased(MouseEvent mouseEvent) {
1907         checkMultiSelect(mouseEvent);
1908       }
1909 
1910       public void mouseEntered(MouseEvent mouseEvent) {
1911         checkMultiSelect(mouseEvent);
1912       }
1913 
1914       public void mouseExited(MouseEvent mouseEvent) {
1915         checkMultiSelect(mouseEvent);
1916       }
1917 
1918       private void checkMultiSelect(MouseEvent mouseEvent) {
1919         if (mouseEvent.isAltDown()) {
1920           searchTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
1921         } else {
1922           searchTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
1923         }
1924       }
1925     });
1926 
1927 
1928     final PopupListener searchTablePopupListener = new PopupListener(searchPopup);
1929     searchPane.addMouseListener(searchTablePopupListener);
1930     searchTable.addMouseListener(searchTablePopupListener);
1931   }
1932 
1933        private String getValueOf(int row, int column) {
1934          if (currentTable == null) {
1935            return "";
1936          }
1937 
1938          Object o = currentTable.getValueAt(row, column);
1939 
1940          if (o instanceof Date) {
1941            return TIMESTAMP_DATE_FORMAT.format((Date)o);
1942          }
1943 
1944          if (o instanceof String) {
1945            return (String)o;
1946          }
1947 
1948          if (o instanceof Level) {
1949            return o.toString();
1950          }
1951 
1952          if (o instanceof String[]) {
1953            String value = "";
1954           //exception - build message + throwable
1955           String[] ti = (String[])o;
1956             if (ti.length > 0 && (!(ti.length == 1 && ti[0].equals("")))) {
1957               LoggingEventWrapper loggingEventWrapper = ((ChainsawCyclicBufferTableModel)(currentTable.getModel())).getRow(row);
1958               value = loggingEventWrapper.getLoggingEvent().getMessage().toString();
1959               for (int i=0;i<((String[])o).length;i++) {
1960                   value = value + "\n" + ((String[]) o)[i];
1961               }
1962             }
1963            return value;
1964          }
1965          return "";
1966       }
1967 
1968     private Action getFindNextAction() {
1969     final Action action =
1970       new AbstractAction("Find next") {
1971         public void actionPerformed(ActionEvent e) {
1972           findNext();
1973         }
1974       };
1975 
1976     //    action.putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_F));
1977     action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("F3"));
1978     action.putValue(
1979       Action.SHORT_DESCRIPTION,
1980       "Find the next occurrence of the rule from the current row");
1981     action.putValue(Action.SMALL_ICON, new ImageIcon(ChainsawIcons.DOWN));
1982 
1983     return action;
1984   }
1985 
1986   private Action getFindPreviousAction() {
1987     final Action action =
1988       new AbstractAction("Find previous") {
1989         public void actionPerformed(ActionEvent e) {
1990             findPrevious();
1991         }
1992       };
1993 
1994     //    action.putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_F));
1995     action.putValue(
1996       Action.ACCELERATOR_KEY,
1997       KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.SHIFT_MASK));
1998     action.putValue(
1999       Action.SHORT_DESCRIPTION,
2000       "Find the previous occurrence of the rule from the current row");
2001     action.putValue(Action.SMALL_ICON, new ImageIcon(ChainsawIcons.UP));
2002 
2003     return action;
2004   }
2005 
2006   private  void buildCombo(final AutoFilterComboBox combo, boolean isFiltering, final AutoFilterComboBox.AutoFilterComboBoxModel otherModel) {
2007     //add (hopefully useful) default filters
2008     combo.addItem("LEVEL == TRACE");
2009     combo.addItem("LEVEL >= DEBUG");
2010     combo.addItem("LEVEL >= INFO");
2011     combo.addItem("LEVEL >= WARN");
2012     combo.addItem("LEVEL >= ERROR");
2013     combo.addItem("LEVEL == FATAL");
2014 
2015     final JTextField filterText =(JTextField) combo.getEditor().getEditorComponent();
2016     if (isFiltering) {
2017       filterText.getDocument().addDocumentListener(new DelayedTextDocumentListener(filterText));
2018     }
2019     filterText.setToolTipText("Enter an expression - right click or ctrl-space for menu - press enter to add to list");
2020     filterText.addKeyListener(new ExpressionRuleContext(filterModel, filterText));
2021 
2022     if (combo.getEditor().getEditorComponent() instanceof JTextField) {
2023       combo.addActionListener(
2024         new AbstractAction() {
2025           public void actionPerformed(ActionEvent e) {
2026             if (e.getActionCommand().equals("comboBoxEdited")) {
2027               try {
2028                 //verify the expression is valid
2029                   Object item = combo.getSelectedItem();
2030                   if (item != null && !item.toString().trim().equals("")) {
2031                     ExpressionRule.getRule(item.toString());
2032                     //add entry as first row of the combo box
2033                     combo.insertItemAt(item, 0);
2034                     otherModel.insertElementAt(item, 0);
2035                   }
2036                 //valid expression, reset background color in case we were previously an invalid expression
2037                 filterText.setBackground(UIManager.getColor("TextField.background"));
2038               } catch (IllegalArgumentException iae) {
2039                   //don't add expressions that aren't valid
2040                   //invalid expression, change background of the field
2041                   filterText.setToolTipText(iae.getMessage());
2042                   filterText.setBackground(ChainsawConstants.INVALID_EXPRESSION_BACKGROUND);
2043               }
2044             }
2045           }
2046         });
2047     }
2048   }
2049 
2050   /**
2051    * Accessor
2052    *
2053    * @return scrollToBottom
2054    *
2055    */
2056   public boolean isScrollToBottom() {
2057   	return preferenceModel.isScrollToBottom();
2058   }
2059 
2060   public void setRefineFocusText(String refineFocusText) {
2061       final JTextField filterText =(JTextField) filterCombo.getEditor().getEditorComponent();
2062       filterText.setText(refineFocusText);
2063   }
2064 
2065   public String getRefineFocusText() {
2066       final JTextField filterText =(JTextField) filterCombo.getEditor().getEditorComponent();
2067       return filterText.getText();
2068   }
2069   /**
2070    * Mutator
2071    *
2072    */
2073   public void toggleScrollToBottom() {
2074   	preferenceModel.setScrollToBottom(!preferenceModel.isScrollToBottom());
2075   }
2076   
2077   private void scrollToBottom() {
2078     //run this in an invokeLater block to ensure this action is enqueued to the end of the EDT
2079     EventQueue.invokeLater(new Runnable()
2080     {
2081         public void run() {
2082           int scrollRow = tableModel.getRowCount() - 1;
2083             table.scrollToRow(scrollRow);
2084         }
2085     });
2086   }
2087 
2088   public void scrollToTop() {
2089       EventQueue.invokeLater(new Runnable() {
2090           public void run() {
2091               if (tableModel.getRowCount() > 1) {
2092                   table.scrollToRow(0);
2093               }
2094           }
2095       });
2096   }
2097 
2098   /**
2099    * Accessor
2100    *
2101    * @return namespace
2102    *
2103    * @see Profileable
2104    */
2105   public String getNamespace() {
2106     return getIdentifier();
2107   }
2108 
2109   /**
2110    * Accessor
2111    *
2112    * @return identifier
2113    *
2114    * @see EventBatchListener
2115    */
2116   public String getInterestedIdentifier() {
2117     return getIdentifier();
2118   }
2119 
2120   /**
2121    * Process events associated with the identifier.  Currently assumes it only
2122    * receives events which share this LogPanel's identifier
2123    *
2124    * @param ident identifier shared by events
2125    * @param events list of LoggingEvent objects
2126    */
2127   public void receiveEventBatch(String ident, final List events) {
2128 
2129     SwingHelper.invokeOnEDT(new Runnable() {
2130       public void run() {
2131         /*
2132         * if this panel is paused, we totally ignore events
2133         */
2134         if (isPaused()) {
2135           return;
2136         }
2137         final int selectedRow = table.getSelectedRow();
2138         final int startingRow = table.getRowCount();
2139         final LoggingEventWrapper selectedEvent;
2140         if (selectedRow >= 0) {
2141           selectedEvent = tableModel.getRow(selectedRow);
2142         } else {
2143           selectedEvent = null;
2144         }
2145 
2146         final int startingSearchRow = searchTable.getRowCount();
2147 
2148         boolean rowAdded = false;
2149         boolean searchRowAdded = false;
2150 
2151         int addedRowCount = 0;
2152         int searchAddedRowCount = 0;
2153 
2154         for (Iterator iter = events.iterator(); iter.hasNext();) {
2155           //these are actual LoggingEvent instances
2156           LoggingEvent event = (LoggingEvent)iter.next();
2157           //create two separate loggingEventWrappers (main table and search table), as they have different info on display state
2158           LoggingEventWrapper loggingEventWrapper1 = new LoggingEventWrapper(event);
2159             //if the clearTableExpressionRule is not null, evaluate & clear the table if it matches
2160             if (clearTableExpressionRule != null && clearTableExpressionRule.evaluate(event, null)) {
2161                 logger.info("clear table expression matched - clearing table - matching event msg - " + event.getMessage());
2162                 clearEvents();
2163             }
2164 
2165           updateOtherModels(event);
2166           boolean isCurrentRowAdded = tableModel.isAddRow(loggingEventWrapper1);
2167           if (isCurrentRowAdded) {
2168               addedRowCount++;
2169           }
2170           rowAdded = rowAdded || isCurrentRowAdded;
2171 
2172           //create a new loggingEventWrapper via copy constructor to ensure same IDs
2173           LoggingEventWrapper loggingEventWrapper2 = new LoggingEventWrapper(loggingEventWrapper1);
2174           boolean isSearchCurrentRowAdded = searchModel.isAddRow(loggingEventWrapper2);
2175           if (isSearchCurrentRowAdded) {
2176               searchAddedRowCount++;
2177           }
2178           searchRowAdded = searchRowAdded || isSearchCurrentRowAdded;
2179         }
2180         //fire after adding all events
2181         if (rowAdded) {
2182           tableModel.fireTableEvent(startingRow, startingRow + addedRowCount, addedRowCount);
2183         }
2184         if (searchRowAdded) {
2185           searchModel.fireTableEvent(startingSearchRow, startingSearchRow + searchAddedRowCount, searchAddedRowCount);
2186         }
2187 
2188         //tell the model to notify the count listeners
2189         tableModel.notifyCountListeners();
2190 
2191         if (rowAdded) {
2192           if (tableModel.isSortEnabled()) {
2193             tableModel.sort();
2194           }
2195 
2196           //always update detail pane (since we may be using a cyclic buffer which is full)
2197           detailPaneUpdater.setSelectedRow(table.getSelectedRow());
2198         }
2199 
2200         if (searchRowAdded) {
2201           if (searchModel.isSortEnabled()) {
2202             searchModel.sort();
2203           }
2204         }
2205 
2206         if (!isScrollToBottom() && selectedEvent != null) {
2207           final int newIndex = tableModel.getRowIndex(selectedEvent);
2208           if (newIndex >= 0) {
2209             // Don't scroll, just maintain selection...
2210             table.setRowSelectionInterval(newIndex, newIndex);
2211           }
2212         }
2213       }
2214     });
2215   }
2216 
2217   /**
2218    * Load settings from the panel preference model
2219    *
2220    * @param event
2221    *
2222    * @see LogPanelPreferenceModel
2223    */
2224   public void loadSettings(LoadSettingsEvent event) {
2225 
2226     File xmlFile = null;
2227     try {
2228       xmlFile = new File(SettingsManager.getInstance().getSettingsDirectory(), URLEncoder.encode(identifier, "UTF-8") + ".xml");
2229     } catch (UnsupportedEncodingException e) {
2230       e.printStackTrace();
2231     }
2232 
2233     if (xmlFile.exists()) {
2234         XStream stream = buildXStreamForLogPanelPreference();
2235         ObjectInputStream in = null;
2236         try {
2237             FileReader r = new FileReader(xmlFile);
2238             in = stream.createObjectInputStream(r);
2239             LogPanelPreferenceModel storedPrefs = (LogPanelPreferenceModel)in.readObject();
2240             lowerPanelDividerLocation = in.readInt();
2241             int treeDividerLocation = in.readInt();
2242             String conversionPattern = in.readObject().toString();
2243             Point p = (Point)in.readObject();
2244             Dimension d = (Dimension)in.readObject();
2245             //this version number is checked to identify whether there is a Vector comming next
2246             int versionNumber = 0;
2247             try {
2248                 versionNumber = in.readInt();
2249             } catch (EOFException eof){
2250             }
2251 
2252             Vector savedVector;
2253             //read the vector only if the version number is greater than 0. higher version numbers can be
2254             //used in the future to save more data structures
2255             if (versionNumber > 0) {
2256                 savedVector = (Vector) in.readObject();
2257                 for(int i = 0 ; i < savedVector.size() ; i++){
2258                     Object item = savedVector.get(i);
2259                     //insert each row at index zero (so last row in vector will be row zero)
2260                     filterCombo.insertItemAt(item, 0);
2261                     findCombo.insertItemAt(item, 0);
2262                 }
2263                 if (versionNumber > 1) {
2264                     //update prefModel columns to include defaults
2265                     int index = 0;
2266                     String columnOrder = event.getSetting(TABLE_COLUMN_ORDER);
2267                     StringTokenizer tok = new StringTokenizer(columnOrder, ",");
2268                     while (tok.hasMoreElements()) {
2269                       String element = tok.nextElement().toString().trim().toUpperCase();
2270                       TableColumn column = new TableColumn(index++);
2271                       column.setHeaderValue(element);
2272                       preferenceModel.addColumn(column);
2273                     }
2274 
2275                     TableColumnModel columnModel = table.getColumnModel();
2276                     //remove previous columns
2277                     while (columnModel.getColumnCount() > 0) {
2278                         columnModel.removeColumn(columnModel.getColumn(0));
2279                     }
2280                     //add visible column order columns
2281                     for (Iterator iter = preferenceModel.getVisibleColumnOrder().iterator();iter.hasNext();) {
2282                         TableColumn col = (TableColumn)iter.next();
2283                         columnModel.addColumn(col);
2284                     }
2285 
2286                   TableColumnModel searchColumnModel = searchTable.getColumnModel();
2287                   //remove previous columns
2288                   while (searchColumnModel.getColumnCount() > 0) {
2289                       searchColumnModel.removeColumn(searchColumnModel.getColumn(0));
2290                   }
2291                   //add visible column order columns
2292                   for (Iterator iter = preferenceModel.getVisibleColumnOrder().iterator();iter.hasNext();) {
2293                       TableColumn col = (TableColumn)iter.next();
2294                       searchColumnModel.addColumn(col);
2295                   }
2296 
2297                     preferenceModel.apply(storedPrefs);
2298                 } else {
2299                     loadDefaultColumnSettings(event);
2300                 }
2301                 //ensure tablemodel cyclic flag is updated
2302                 //may be panel configs that don't have these values
2303                 tableModel.setCyclic(preferenceModel.isCyclic());
2304                 searchModel.setCyclic(preferenceModel.isCyclic());
2305                 lowerPanel.setDividerLocation(lowerPanelDividerLocation);
2306                 nameTreeAndMainPanelSplit.setDividerLocation(treeDividerLocation);
2307                 detailLayout.setConversionPattern(conversionPattern);
2308                 if (p.x != 0 && p.y != 0) {
2309                     undockedFrame.setLocation(p.x, p.y);
2310                     undockedFrame.setSize(d);
2311                 } else {
2312                     undockedFrame.setLocation(0, 0);
2313                     undockedFrame.setSize(new Dimension(1024, 768));
2314                 }
2315             } else {
2316                 loadDefaultColumnSettings(event);
2317             }
2318         } catch (Exception e) {
2319             e.printStackTrace();
2320             loadDefaultColumnSettings(event);
2321             // TODO need to log this..
2322         } finally {
2323             if (in != null) {
2324                 try {
2325                     in.close();
2326                 } catch (IOException ioe) {}
2327             }
2328         }
2329     } else {
2330         //not setting lower panel divider location here - will do that after the UI is visible
2331         loadDefaultColumnSettings(event);
2332     }
2333     //ensure tablemodel cyclic flag is updated
2334     tableModel.setCyclic(preferenceModel.isCyclic());
2335     searchModel.setCyclic(preferenceModel.isCyclic());
2336     logTreePanel.ignore(preferenceModel.getHiddenLoggers());
2337     logTreePanel.setHiddenExpression(preferenceModel.getHiddenExpression());
2338     logTreePanel.setAlwaysDisplayExpression(preferenceModel.getAlwaysDisplayExpression());
2339     if (preferenceModel.getClearTableExpression() != null) {
2340         try {
2341             clearTableExpressionRule = ExpressionRule.getRule(preferenceModel.getClearTableExpression());
2342         } catch (Exception e) {
2343             clearTableExpressionRule = null;
2344         }
2345     }
2346 
2347     //attempt to load color settings - no need to URL encode the identifier
2348     colorizer.loadColorSettings(identifier);
2349   }
2350 
2351   /**
2352    * Save preferences to the panel preference model
2353    *
2354    * @param event
2355    *
2356    * @see LogPanelPreferenceModel
2357    */
2358   public void saveSettings(SaveSettingsEvent event) {
2359     File xmlFile = null;
2360     try {
2361       xmlFile = new File(SettingsManager.getInstance().getSettingsDirectory(), URLEncoder.encode(identifier, "UTF-8") + ".xml");
2362     } catch (UnsupportedEncodingException e) {
2363       e.printStackTrace();
2364       //unable to save..just return
2365       return;
2366     }
2367 
2368     preferenceModel.setHiddenLoggers(new HashSet(logTreePanel.getHiddenSet()));
2369     preferenceModel.setHiddenExpression(logTreePanel.getHiddenExpression());
2370     preferenceModel.setAlwaysDisplayExpression(logTreePanel.getAlwaysDisplayExpression());
2371     List visibleOrder = new ArrayList();
2372     Enumeration cols = table.getColumnModel().getColumns();
2373     while (cols.hasMoreElements()) {
2374     	TableColumn c = (TableColumn)cols.nextElement();
2375     	visibleOrder.add(c);
2376     }
2377     preferenceModel.setVisibleColumnOrder(visibleOrder);
2378     //search table will use same columns as main table
2379     
2380     XStream stream = buildXStreamForLogPanelPreference();
2381     ObjectOutputStream s = null;
2382     try {
2383     	FileWriter w = new FileWriter(xmlFile);
2384     	s = stream.createObjectOutputStream(w);
2385     	s.writeObject(preferenceModel);
2386       if (isDetailPanelVisible) {
2387         //use current size
2388         s.writeInt(lowerPanel.getDividerLocation());
2389       } else {
2390         //use size when last hidden
2391         s.writeInt(lowerPanelDividerLocation);
2392       }
2393     	s.writeInt(nameTreeAndMainPanelSplit.getDividerLocation());
2394     	s.writeObject(detailLayout.getConversionPattern());
2395     	s.writeObject(undockedFrame.getLocation());
2396     	s.writeObject(undockedFrame.getSize());
2397         //this is a version number written to the file to identify that there is a Vector serialized after this
2398         s.writeInt(LOG_PANEL_SERIALIZATION_VERSION_NUMBER);
2399         //don't write filterexpressionvector, write the combobox's model's backing vector
2400         Vector combinedVector = new Vector();
2401         combinedVector.addAll(filterCombo.getModelData());
2402         combinedVector.addAll(findCombo.getModelData());
2403         //duplicates will be removed when loaded..
2404         s.writeObject(combinedVector);
2405     } catch (Exception ex) {
2406         ex.printStackTrace();
2407         // TODO need to log this..
2408     } finally {
2409     	if (s != null) {
2410     		try {
2411     			s.close();
2412     		} catch (IOException ioe) {}
2413     	}
2414     }
2415 
2416     //no need to URL encode the identifier
2417     colorizer.saveColorSettings(identifier);
2418   }
2419 
2420     private XStream buildXStreamForLogPanelPreference() {
2421         XStream stream = new XStream(new DomDriver());
2422         stream.registerConverter(new TableColumnConverter());
2423         return stream;
2424     }
2425 
2426   /**
2427      * Display the panel preferences frame
2428      */
2429   void showPreferences() {
2430       //don't pack this frame
2431       centerAndSetVisible(logPanelPreferencesFrame);
2432   }
2433 
2434   public static void centerAndSetVisible(Window window) {
2435     Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize();
2436     window.setLocation(new Point((screenDimension.width / 2) - (window.getSize().width / 2),
2437       (screenDimension.height / 2) - (window.getSize().height / 2)));
2438     window.setVisible(true);
2439   }
2440 
2441   /**
2442    * Display the color rule frame
2443    */
2444   void showColorPreferences() {
2445     colorPanel.loadLogPanelColorizers();
2446     colorFrame.pack();
2447     centerAndSetVisible(colorFrame);
2448   }
2449 
2450   /**
2451    * Toggle panel preference for detail visibility on or off
2452    */
2453   void toggleDetailVisible() {
2454     preferenceModel.setDetailPaneVisible(
2455       !preferenceModel.isDetailPaneVisible());
2456   }
2457 
2458   /**
2459    * Accessor
2460    *
2461    * @return detail visibility flag
2462    */
2463   boolean isDetailVisible() {
2464     return preferenceModel.isDetailPaneVisible();
2465   }
2466 
2467   boolean isSearchResultsVisible() {
2468     return preferenceModel.isSearchResultsVisible();
2469   }
2470 
2471   /**
2472    * Toggle panel preference for logger tree visibility on or off
2473    */
2474   void toggleLogTreeVisible() {
2475     preferenceModel.setLogTreePanelVisible(
2476       !preferenceModel.isLogTreePanelVisible());
2477   }
2478 
2479   /**
2480    * Accessor
2481    *
2482    * @return logger tree visibility flag
2483    */
2484   boolean isLogTreeVisible() {
2485     return preferenceModel.isLogTreePanelVisible();
2486   }
2487 
2488   /**
2489    * Return all events
2490    *
2491    * @return list of LoggingEvents
2492    */
2493   List getEvents() {
2494     return tableModel.getAllEvents();
2495   }
2496 
2497   /**
2498    * Return the events that are visible with the current filter applied
2499    *
2500    * @return list of LoggingEvents
2501    */
2502   List getFilteredEvents() {
2503   	return tableModel.getFilteredEvents();
2504   }
2505   
2506   List getMatchingEvents(Rule rule) {
2507     return tableModel.getMatchingEvents(rule);
2508   }
2509 
2510   /**
2511    * Remove all events
2512    */
2513   void clearEvents() {
2514     clearModel();
2515   }
2516 
2517   /**
2518    * Accessor
2519    *
2520    * @return identifier
2521    */
2522   String getIdentifier() {
2523     return identifier;
2524   }
2525 
2526   /**
2527    * Undocks this DockablePanel by removing the panel from the LogUI window
2528    * and placing it inside it's own JFrame.
2529    */
2530   void undock() {
2531   	final int row = table.getSelectedRow();
2532     setDocked(false);
2533     externalPanel.removeAll();
2534 
2535     externalPanel.add(undockedToolbar, BorderLayout.NORTH);
2536     externalPanel.add(nameTreeAndMainPanelSplit, BorderLayout.CENTER);
2537     externalPanel.setDocked(false);
2538     undockedFrame.pack();
2539 
2540     undockedFrame.setVisible(true);
2541     dockingAction.putValue(Action.NAME, "Dock");
2542     dockingAction.putValue(Action.SMALL_ICON, ChainsawIcons.ICON_DOCK);
2543     if (row > -1) {
2544         EventQueue.invokeLater(new Runnable() {
2545             public void run() {
2546                 table.scrollToRow(row);
2547             }
2548         });
2549     }
2550   }
2551 
2552   /**
2553    * Add an eventCountListener
2554    *
2555    * @param l
2556    */
2557   void addEventCountListener(EventCountListener l) {
2558     tableModel.addEventCountListener(l);
2559   }
2560 
2561   /**
2562    * Accessor
2563    *
2564    * @return paused flag
2565    */
2566   boolean isPaused() {
2567     return paused;
2568   }
2569 
2570   /**
2571    * Modifies the Paused property and notifies the listeners
2572    *
2573    * @param paused
2574    */
2575   void setPaused(boolean paused) {
2576     boolean oldValue = this.paused;
2577     this.paused = paused;
2578     firePropertyChange("paused", oldValue, paused);
2579   }
2580 
2581   /**
2582    * Change the selected event on the log panel.  Will cause scrollToBottom to be turned off.
2583    *
2584    * @param eventNumber
2585    * @return row number or -1 if row with log4jid property with that number was not found
2586    */
2587   int setSelectedEvent(int eventNumber) {
2588       int row = tableModel.locate(ExpressionRule.getRule("prop.log4jid == " + eventNumber), 0, true);
2589       if (row > -1) {
2590         preferenceModel.setScrollToBottom(false);
2591 
2592         table.scrollToRow(row);
2593       }
2594       return row;
2595   }
2596 
2597   /**
2598    * Add a preference propertyChangeListener
2599    *
2600    * @param listener
2601    */
2602   void addPreferencePropertyChangeListener(PropertyChangeListener listener) {
2603     preferenceModel.addPropertyChangeListener(listener);
2604   }
2605 
2606   /**
2607    * Toggle the LoggingEvent container from either managing a cyclic buffer of
2608    * events or an ArrayList of events
2609    */
2610   void toggleCyclic() {
2611     boolean toggledCyclic = !preferenceModel.isCyclic();
2612 
2613     preferenceModel.setCyclic(toggledCyclic);
2614     tableModel.setCyclic(toggledCyclic);
2615     searchModel.setCyclic(toggledCyclic);
2616   }
2617 
2618   /**
2619    * Accessor
2620    *
2621    * @return flag answering if LoggingEvent container is a cyclic buffer
2622    */
2623   boolean isCyclic() {
2624     return preferenceModel.isCyclic();
2625   }
2626 
2627   public void updateFindRule(String ruleText) {
2628     if ((ruleText == null) || (ruleText.trim().equals(""))) {
2629       findRule = null;
2630       tableModel.updateEventsWithFindRule(null);
2631       colorizer.setFindRule(null);
2632       tableRuleMediator.setFindRule(null);
2633       searchRuleMediator.setFindRule(null);
2634       //reset background color in case we were previously an invalid expression
2635       findCombo.setBackground(UIManager.getColor("TextField.background"));
2636       findCombo.setToolTipText(
2637         "Enter an expression - right click or ctrl-space for menu - press enter to add to list");
2638       currentSearchMatchCount = 0;
2639       currentFindRuleText = null;
2640       statusBar.setSearchMatchCount(currentSearchMatchCount, getIdentifier());
2641       //if the preference to show search results is enabled, the find rule is now null - hide search results
2642       if (isSearchResultsVisible()) {
2643         hideSearchResults();
2644       }
2645     } else {
2646       //only turn off scrolltobottom when finding something (find not empty)
2647       preferenceModel.setScrollToBottom(false);
2648       if(ruleText.equals(currentFindRuleText)) {
2649           //don't update events if rule hasn't changed (we're finding next/previous)
2650           return;
2651       }
2652       currentFindRuleText = ruleText;
2653       try {
2654         final JTextField findText =(JTextField) findCombo.getEditor().getEditorComponent();
2655         findText.setToolTipText(
2656           "Enter an expression - right click or ctrl-space for menu - press enter to add to list");
2657         findRule = ExpressionRule.getRule(ruleText);
2658         currentSearchMatchCount = tableModel.updateEventsWithFindRule(findRule);
2659         searchModel.updateEventsWithFindRule(findRule);
2660         colorizer.setFindRule(findRule);
2661         tableRuleMediator.setFindRule(findRule);
2662         searchRuleMediator.setFindRule(findRule);
2663         //valid expression, reset background color in case we were previously an invalid expression
2664         findText.setBackground(UIManager.getColor("TextField.background"));
2665         statusBar.setSearchMatchCount(currentSearchMatchCount, getIdentifier());
2666         if (isSearchResultsVisible()) {
2667           showSearchResults();
2668         }
2669       } catch (IllegalArgumentException re) {
2670         findRule = null;
2671         final JTextField findText =(JTextField) findCombo.getEditor().getEditorComponent();
2672         findText.setToolTipText(re.getMessage());
2673         findText.setBackground(ChainsawConstants.INVALID_EXPRESSION_BACKGROUND);
2674         colorizer.setFindRule(null);
2675         tableRuleMediator.setFindRule(null);
2676         searchRuleMediator.setFindRule(null);
2677         tableModel.updateEventsWithFindRule(null);
2678         searchModel.updateEventsWithFindRule(null);
2679         currentSearchMatchCount = 0;
2680         statusBar.setSearchMatchCount(currentSearchMatchCount, getIdentifier());
2681         //if the preference to show search results is enabled, the find rule is now null - hide search results
2682         if (isSearchResultsVisible()) {
2683           hideSearchResults();
2684         }
2685       }
2686     }
2687   }
2688 
2689   private void hideSearchResults() {
2690     if (searchResultsDisplayed) {
2691       detailPanel.removeAll();
2692       JPanel leftSpacePanel = new JPanel();
2693       Integer scrollBarWidth = (Integer) UIManager.get("ScrollBar.width");
2694       leftSpacePanel.setPreferredSize(new Dimension(scrollBarWidth.intValue() -4, -1));
2695 
2696       JPanel rightSpacePanel = new JPanel();
2697       rightSpacePanel.setPreferredSize(new Dimension(scrollBarWidth.intValue() -4, -1));
2698 
2699       detailPanel.add(detailToolbar, BorderLayout.NORTH);
2700       detailPanel.add(detailPane, BorderLayout.CENTER);
2701 
2702       detailPanel.add(leftSpacePanel, BorderLayout.WEST);
2703       detailPanel.add(rightSpacePanel, BorderLayout.EAST);
2704  
2705       detailPanel.revalidate();
2706       detailPanel.repaint();
2707       //if the detail visible pref is not enabled, hide the detail pane
2708       searchResultsDisplayed = false;
2709       //hide if pref is not enabled
2710       if (!isDetailVisible()) {
2711         hideDetailPane();
2712       }
2713     }
2714   }
2715 
2716   private void showSearchResults() {
2717     if (isSearchResultsVisible() && !searchResultsDisplayed && findRule != null) {
2718       //if pref is set, always update detail panel to contain search results
2719       detailPanel.removeAll();
2720       detailPanel.add(searchPane, BorderLayout.CENTER);
2721       Integer scrollBarWidth = (Integer) UIManager.get("ScrollBar.width");
2722       JPanel leftSpacePanel = new JPanel();
2723       leftSpacePanel.setPreferredSize(new Dimension(scrollBarWidth.intValue() -4, -1));
2724       JPanel rightSpacePanel = new JPanel();
2725       rightSpacePanel.setPreferredSize(new Dimension(scrollBarWidth.intValue() -4, -1));
2726       detailPanel.add(leftSpacePanel, BorderLayout.WEST);
2727       detailPanel.add(rightSpacePanel, BorderLayout.EAST);
2728       detailPanel.revalidate();
2729       detailPanel.repaint();
2730       //if the detail visible pref is not enabled, show the detail pane
2731       searchResultsDisplayed = true;
2732       //show if pref is not enabled
2733       if (!isDetailVisible()) {
2734         showDetailPane();
2735       }
2736     }
2737   }
2738 
2739   /**
2740    * Display the detail pane, using the last known divider location
2741    */
2742   private void showDetailPane() {
2743     if (!isDetailPanelVisible) {
2744       lowerPanel.setDividerSize(dividerSize);
2745       if (lowerPanelDividerLocation == 0) {
2746         lowerPanel.setDividerLocation(DEFAULT_DETAIL_SPLIT_LOCATION);
2747         lowerPanelDividerLocation = lowerPanel.getDividerLocation();
2748       } else {
2749         lowerPanel.setDividerLocation(lowerPanelDividerLocation);
2750       }
2751       detailPanel.setVisible(true);
2752       detailPanel.repaint();
2753       lowerPanel.repaint();
2754       isDetailPanelVisible = true;
2755     }
2756   }
2757 
2758   /**
2759    * Hide the detail pane, holding the current divider location for later use
2760    */
2761   private void hideDetailPane() {
2762     //may be called not currently visible on initial setup to ensure panel is not visible..only update divider location if hiding when currently visible
2763     if (isDetailPanelVisible) {
2764       lowerPanelDividerLocation = lowerPanel.getDividerLocation();
2765     }
2766     lowerPanel.setDividerSize(0);
2767     detailPanel.setVisible(false);
2768     lowerPanel.repaint();
2769     isDetailPanelVisible = false;
2770   }
2771 
2772   /**
2773    * Display the log tree pane, using the last known divider location
2774    */
2775   private void showLogTreePanel() {
2776     nameTreeAndMainPanelSplit.setDividerSize(dividerSize);
2777     nameTreeAndMainPanelSplit.setDividerLocation(
2778       lastLogTreePanelSplitLocation);
2779     logTreePanel.setVisible(true);
2780     nameTreeAndMainPanelSplit.repaint();
2781   }
2782 
2783   /**
2784    * Hide the log tree pane, holding the current divider location for later use
2785    */
2786   private void hideLogTreePanel() {
2787     //subtract one to make sizes match
2788     int currentSize = nameTreeAndMainPanelSplit.getWidth() - nameTreeAndMainPanelSplit.getDividerSize() - 1;
2789 
2790     if (currentSize > 0) {
2791       lastLogTreePanelSplitLocation =
2792         (double) nameTreeAndMainPanelSplit.getDividerLocation() / currentSize;
2793     }
2794     nameTreeAndMainPanelSplit.setDividerSize(0);
2795     logTreePanel.setVisible(false);
2796     nameTreeAndMainPanelSplit.repaint();
2797   }
2798 
2799   /**
2800    * Return a toolbar used by the undocked LogPanel's frame
2801    *
2802    * @return toolbar
2803    */
2804   private JToolBar createDockwindowToolbar() {
2805     final JToolBar toolbar = new JToolBar();
2806     toolbar.setFloatable(false);
2807 
2808     final Action dockPauseAction =
2809       new AbstractAction("Pause") {
2810         public void actionPerformed(ActionEvent evt) {
2811           setPaused(!isPaused());
2812         }
2813       };
2814 
2815     dockPauseAction.putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_P));
2816     dockPauseAction.putValue(
2817       Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("F12"));
2818     dockPauseAction.putValue(
2819       Action.SHORT_DESCRIPTION,
2820       "Halts the display, while still allowing events to stream in the background");
2821     dockPauseAction.putValue(
2822       Action.SMALL_ICON, new ImageIcon(ChainsawIcons.PAUSE));
2823 
2824     final SmallToggleButton dockPauseButton =
2825       new SmallToggleButton(dockPauseAction);
2826     dockPauseButton.setText("");
2827 
2828     dockPauseButton.getModel().setSelected(isPaused());
2829 
2830     addPropertyChangeListener(
2831       "paused",
2832       new PropertyChangeListener() {
2833         public void propertyChange(PropertyChangeEvent evt) {
2834           dockPauseButton.getModel().setSelected(isPaused());
2835         }
2836       });
2837     toolbar.add(dockPauseButton);
2838 
2839     Action dockShowPrefsAction =
2840       new AbstractAction("") {
2841         public void actionPerformed(ActionEvent arg0) {
2842           showPreferences();
2843         }
2844       };
2845 
2846     dockShowPrefsAction.putValue(
2847       Action.SHORT_DESCRIPTION, "Define preferences...");
2848     dockShowPrefsAction.putValue(
2849       Action.SMALL_ICON, ChainsawIcons.ICON_PREFERENCES);
2850 
2851     toolbar.add(new SmallButton(dockShowPrefsAction));
2852 
2853     Action dockToggleLogTreeAction =
2854       new AbstractAction() {
2855         public void actionPerformed(ActionEvent e) {
2856           toggleLogTreeVisible();
2857         }
2858       };
2859 
2860       dockToggleLogTreeAction.putValue(Action.SHORT_DESCRIPTION, "Toggles the Logger Tree Pane");
2861       dockToggleLogTreeAction.putValue("enabled", Boolean.TRUE);
2862       dockToggleLogTreeAction.putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_T));
2863       dockToggleLogTreeAction.putValue(
2864         Action.ACCELERATOR_KEY,
2865         KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
2866       dockToggleLogTreeAction.putValue(
2867         Action.SMALL_ICON, new ImageIcon(ChainsawIcons.WINDOW_ICON));
2868 
2869     final SmallToggleButton toggleLogTreeButton =
2870       new SmallToggleButton(dockToggleLogTreeAction);
2871     preferenceModel.addPropertyChangeListener("logTreePanelVisible", new PropertyChangeListener() {
2872     	public void propertyChange(PropertyChangeEvent evt) {
2873     	    toggleLogTreeButton.setSelected(preferenceModel.isLogTreePanelVisible());    		
2874     	}
2875     });
2876     		
2877     toggleLogTreeButton.setSelected(isLogTreeVisible());
2878     toolbar.add(toggleLogTreeButton);
2879     toolbar.addSeparator();
2880 
2881     final Action undockedClearAction =
2882       new AbstractAction("Clear") {
2883         public void actionPerformed(ActionEvent arg0) {
2884           clearModel();
2885         }
2886       };
2887 
2888     undockedClearAction.putValue(
2889       Action.SMALL_ICON, new ImageIcon(ChainsawIcons.DELETE));
2890     undockedClearAction.putValue(
2891       Action.SHORT_DESCRIPTION, "Removes all the events from the current view");
2892 
2893     final SmallButton dockClearButton = new SmallButton(undockedClearAction);
2894     dockClearButton.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
2895       KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()),
2896       undockedClearAction.getValue(Action.NAME));
2897     dockClearButton.getActionMap().put(
2898       undockedClearAction.getValue(Action.NAME), undockedClearAction);
2899 
2900     dockClearButton.setText("");
2901     toolbar.add(dockClearButton);
2902     toolbar.addSeparator();
2903 
2904     Action dockToggleScrollToBottomAction =
2905         new AbstractAction("Toggles Scroll to Bottom") {
2906           public void actionPerformed(ActionEvent e) {
2907             toggleScrollToBottom();
2908           }
2909         };
2910 
2911         dockToggleScrollToBottomAction.putValue(Action.SHORT_DESCRIPTION, "Toggles Scroll to Bottom");
2912         dockToggleScrollToBottomAction.putValue("enabled", Boolean.TRUE);
2913         dockToggleScrollToBottomAction.putValue(
2914           Action.SMALL_ICON, new ImageIcon(ChainsawIcons.SCROLL_TO_BOTTOM));
2915 
2916       final SmallToggleButton toggleScrollToBottomButton =
2917         new SmallToggleButton(dockToggleScrollToBottomAction);
2918       preferenceModel.addPropertyChangeListener("scrollToBottom", new PropertyChangeListener() {
2919       	public void propertyChange(PropertyChangeEvent evt) {
2920       	    toggleScrollToBottomButton.setSelected(isScrollToBottom());    		
2921       	}
2922       });
2923 
2924       toggleScrollToBottomButton.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
2925   	      KeyStroke.getKeyStroke(KeyEvent.VK_B, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()),
2926   	      dockToggleScrollToBottomAction.getValue(Action.NAME));
2927   	    toggleScrollToBottomButton.getActionMap().put(
2928   	      dockToggleScrollToBottomAction.getValue(Action.NAME), dockToggleScrollToBottomAction);
2929       
2930       toggleScrollToBottomButton.setSelected(isScrollToBottom());
2931       toggleScrollToBottomButton.setText("");
2932       toolbar.add(toggleScrollToBottomButton);
2933       toolbar.addSeparator();
2934 
2935     findCombo.addActionListener(new ActionListener(){
2936         public void actionPerformed(ActionEvent e) {
2937           //comboboxchanged event received when text is modified in the field..when enter is pressed, it's comboboxedited
2938           if (e.getActionCommand().equalsIgnoreCase("comboBoxEdited")) {
2939               findNext();
2940           }
2941         }
2942     });
2943     Action redockAction =
2944       new AbstractAction("", ChainsawIcons.ICON_DOCK) {
2945         public void actionPerformed(ActionEvent arg0) {
2946           dock();
2947         }
2948       };
2949 
2950     redockAction.putValue(
2951       Action.SHORT_DESCRIPTION,
2952       "Docks this window back with the main Chainsaw window");
2953 
2954     SmallButton redockButton = new SmallButton(redockAction);
2955     toolbar.add(redockButton);
2956 
2957     return toolbar;
2958   }
2959 
2960   /**
2961    * Update the status bar with current selected row and row count
2962    */
2963   protected void updateStatusBar() {
2964     SwingHelper.invokeOnEDT(
2965       new Runnable() {
2966         public void run() {
2967           statusBar.setSelectedLine(
2968             table.getSelectedRow() + 1, tableModel.getRowCount(),
2969             tableModel.size(), getIdentifier());
2970           statusBar.setSearchMatchCount(currentSearchMatchCount, getIdentifier());
2971         }
2972       });
2973   }
2974 
2975   /**
2976    * Update the detail pane layout text
2977    *
2978    * @param conversionPattern layout text
2979    */
2980   private void setDetailPaneConversionPattern(String conversionPattern) {
2981     String oldPattern = getDetailPaneConversionPattern();
2982     (detailLayout).setConversionPattern(conversionPattern);
2983     firePropertyChange(
2984       "detailPaneConversionPattern", oldPattern,
2985       getDetailPaneConversionPattern());
2986   }
2987 
2988   /**
2989    * Accessor
2990    *
2991    * @return conversionPattern layout text
2992    */
2993   private String getDetailPaneConversionPattern() {
2994     return (detailLayout).getConversionPattern();
2995   }
2996 
2997   /**
2998    * Reset the LoggingEvent container, detail panel and status bar
2999    */
3000   private void clearModel() {
3001     previousLastIndex = -1;
3002     tableModel.clearModel();
3003     searchModel.clearModel();
3004 
3005     synchronized (detail) {
3006       detailPaneUpdater.setSelectedRow(-1);
3007       detail.notify();
3008     }
3009 
3010     statusBar.setNothingSelected();
3011   }
3012 
3013   public void findNextColorizedEvent() {
3014     EventQueue.invokeLater(new Runnable() {
3015         public void run() {
3016             final int nextRow = tableModel.findColoredRow(table.getSelectedRow() + 1, true);
3017             if (nextRow > -1) {
3018                 table.scrollToRow(nextRow);
3019             }
3020         }
3021     });
3022   }
3023 
3024   public void findPreviousColorizedEvent() {
3025     EventQueue.invokeLater(new Runnable() {
3026         public void run() {
3027             final int previousRow = tableModel.findColoredRow(table.getSelectedRow() - 1, false);
3028             if (previousRow > -1) {
3029                 table.scrollToRow(previousRow);
3030             }
3031         }
3032     });
3033   }
3034 
3035   /**
3036    * Finds the next row matching the current find rule, and ensures it is made
3037    * visible
3038    *
3039    */
3040   public void findNext() {
3041     Object item = findCombo.getSelectedItem();
3042     updateFindRule(item == null ? null: item.toString());
3043 
3044     if (findRule != null) {
3045         EventQueue.invokeLater(new Runnable() {
3046             public void run() {
3047               final JTextField findText =(JTextField) findCombo.getEditor().getEditorComponent();
3048                 try {
3049                   int filteredEventsSize = getFilteredEvents().size();
3050                   int startRow = table.getSelectedRow() + 1;
3051                     if (startRow > filteredEventsSize - 1) {
3052                         startRow = 0;
3053                     }
3054                   //no selected row would return -1, so we'd start at row zero
3055                   final int nextRow = tableModel.locate(findRule, startRow, true);
3056 
3057                   if (nextRow > -1) {
3058                     table.scrollToRow(nextRow);
3059                     findText.setToolTipText("Enter an expression - right click or ctrl-space for menu - press enter to add to list");
3060                   }
3061                   findText.setBackground(UIManager.getColor("TextField.background"));
3062                 } catch (IllegalArgumentException iae) {
3063                   findText.setToolTipText(iae.getMessage());
3064                   findText.setBackground(ChainsawConstants.INVALID_EXPRESSION_BACKGROUND);
3065                   colorizer.setFindRule(null);
3066                   tableRuleMediator.setFindRule(null);
3067                   searchRuleMediator.setFindRule(null);
3068                 }
3069             }
3070         });
3071     }
3072   }
3073 
3074   /**
3075    * Finds the previous row matching the current find rule, and ensures it is made
3076    * visible
3077    *
3078    */
3079   public void findPrevious() {
3080     Object item = findCombo.getSelectedItem();
3081     updateFindRule(item == null ? null: item.toString());
3082 
3083     if (findRule != null) {
3084         EventQueue.invokeLater(new Runnable() {
3085             public void run() {
3086               final JTextField findText =(JTextField) findCombo.getEditor().getEditorComponent();
3087                 try {
3088                     int startRow = table.getSelectedRow() - 1;
3089                     int filteredEventsSize = getFilteredEvents().size();
3090                     if (startRow < 0) {
3091                         startRow = filteredEventsSize - 1;
3092                     }
3093                     final int previousRow = tableModel.locate(findRule, startRow, false);
3094 
3095                     if (previousRow > -1) {
3096                         table.scrollToRow(previousRow);
3097                         findCombo.setToolTipText("Enter an expression - right click or ctrl-space for menu - press enter to add to list");
3098                     }
3099                   findText.setBackground(UIManager.getColor("TextField.background"));
3100                 } catch (IllegalArgumentException iae) {
3101                   findText.setToolTipText(iae.getMessage());
3102                   findText.setBackground(ChainsawConstants.INVALID_EXPRESSION_BACKGROUND);
3103                 }
3104             }
3105         });
3106     }
3107   }
3108 
3109   /**
3110    * Docks this DockablePanel by hiding the JFrame and placing the Panel back
3111    * inside the LogUI window.
3112    */
3113   private void dock() {
3114   	
3115   	final int row = table.getSelectedRow();
3116     setDocked(true);
3117     undockedFrame.setVisible(false);
3118     removeAll();
3119 
3120     add(nameTreeAndMainPanelSplit, BorderLayout.CENTER);
3121     externalPanel.setDocked(true);
3122     dockingAction.putValue(Action.NAME, "Undock");
3123     dockingAction.putValue(Action.SMALL_ICON, ChainsawIcons.ICON_UNDOCK);
3124     if (row > -1) {
3125         EventQueue.invokeLater(new Runnable() {
3126             public void run() {
3127                 table.scrollToRow(row);
3128             }
3129         });
3130     }
3131   }
3132 
3133   /**
3134    * Load default column settings if no settings exist for this identifier
3135    *
3136    * @param event
3137    */
3138   private void loadDefaultColumnSettings(LoadSettingsEvent event) {
3139     String columnOrder = event.getSetting(TABLE_COLUMN_ORDER);
3140 
3141     TableColumnModel columnModel = table.getColumnModel();
3142     TableColumnModel searchColumnModel = searchTable.getColumnModel();
3143 
3144     Map columnNameMap = new HashMap();
3145     Map searchColumnNameMap = new HashMap();
3146 
3147     for (int i = 0; i < columnModel.getColumnCount(); i++) {
3148       columnNameMap.put(table.getColumnName(i).toUpperCase(), columnModel.getColumn(i));
3149     }
3150 
3151     for (int i = 0; i < searchColumnModel.getColumnCount(); i++) {
3152       searchColumnNameMap.put(searchTable.getColumnName(i).toUpperCase(), searchColumnModel.getColumn(i));
3153     }
3154 
3155     int index = 0;
3156     StringTokenizer tok = new StringTokenizer(columnOrder, ",");
3157     List sortedColumnList = new ArrayList();
3158 
3159     /*
3160        remove all columns from the table that exist in the model
3161        and add in the correct order to a new arraylist
3162        (may be a subset of possible columns)
3163      **/
3164     while (tok.hasMoreElements()) {
3165       String element = tok.nextElement().toString().trim().toUpperCase();
3166       TableColumn column = (TableColumn) columnNameMap.get(element);
3167 
3168       if (column != null) {
3169         sortedColumnList.add(column);
3170         table.removeColumn(column);
3171         searchTable.removeColumn(column);
3172       }
3173     }
3174     preferenceModel.setDetailPaneVisible(event.asBoolean("detailPaneVisible"));
3175     preferenceModel.setLogTreePanelVisible(event.asBoolean("logTreePanelVisible"));
3176     preferenceModel.setHighlightSearchMatchText(event.asBoolean("highlightSearchMatchText"));
3177     preferenceModel.setWrapMessage(event.asBoolean("wrapMessage"));
3178     preferenceModel.setSearchResultsVisible(event.asBoolean("searchResultsVisible"));
3179     //re-add columns to the table in the order provided from the list
3180     for (Iterator iter = sortedColumnList.iterator(); iter.hasNext();) {
3181       TableColumn element = (TableColumn) iter.next();
3182       if (preferenceModel.addColumn(element)) {
3183           if (!applicationPreferenceModel.isDefaultColumnsSet() || applicationPreferenceModel.isDefaultColumnsSet() &&
3184               applicationPreferenceModel.getDefaultColumnNames().contains(element.getHeaderValue())) {
3185             table.addColumn(element);
3186             searchTable.addColumn(element);
3187             preferenceModel.setColumnVisible(element.getHeaderValue().toString(), true);
3188           }
3189       }
3190     }
3191 
3192     String columnWidths = event.getSetting(TABLE_COLUMN_WIDTHS);
3193 
3194     tok = new StringTokenizer(columnWidths, ",");
3195     index = 0;
3196 
3197     while (tok.hasMoreElements()) {
3198       String element = (String) tok.nextElement();
3199 
3200       try {
3201         int width = Integer.parseInt(element);
3202 
3203         if (index > (columnModel.getColumnCount() - 1)) {
3204           logger.warn(
3205             "loadsettings - failed attempt to set width for index " + index
3206             + ", width " + element);
3207         } else {
3208           columnModel.getColumn(index).setPreferredWidth(width);
3209           searchColumnModel.getColumn(index).setPreferredWidth(width);
3210         }
3211 
3212         index++;
3213       } catch (NumberFormatException e) {
3214         logger.error("Error decoding a Table width", e);
3215       }
3216     }
3217     undockedFrame.setSize(getSize());
3218     undockedFrame.setLocation(getBounds().x, getBounds().y);
3219 
3220       repaint();
3221     }
3222 
3223   /**
3224    * Iterate over all values in the column and return the longest width
3225    *
3226    * @param index column index
3227    *
3228    * @return longest width - relies on FontMetrics.stringWidth for calculation
3229    */
3230   private int getMaxColumnWidth(int index) {
3231     FontMetrics metrics = getGraphics().getFontMetrics();
3232     int longestWidth =
3233       metrics.stringWidth("  " + table.getColumnName(index) + "  ")
3234       + (2 * table.getColumnModel().getColumnMargin());
3235 
3236     for (int i = 0, j = tableModel.getRowCount(); i < j; i++) {
3237       Component c =
3238         renderer.getTableCellRendererComponent(
3239           table, table.getValueAt(i, index), false, false, i, index);
3240 
3241       if (c instanceof JLabel) {
3242         longestWidth =
3243           Math.max(longestWidth, metrics.stringWidth(((JLabel) c).getText()));
3244       }
3245     }
3246 
3247     return longestWidth + 5;
3248   }
3249 
3250   private String getToolTipTextForEvent(LoggingEventWrapper loggingEventWrapper) {
3251     StringBuffer buf = new StringBuffer();
3252     buf.append(detailLayout.getHeader()).append(detailLayout.format(loggingEventWrapper.getLoggingEvent())).append(detailLayout.getFooter());
3253     return buf.toString();
3254   }
3255 
3256   /**
3257    * ensures the Entry map of all the unque logger names etc, that is used for
3258    * the Filter panel is updated with any new information from the event
3259    *
3260    * @param event
3261    */
3262   private void updateOtherModels(LoggingEvent event) {
3263 
3264     /*
3265      * EventContainer is a LoggerNameModel imp, use that for notifing
3266      */
3267     tableModel.addLoggerName(event.getLoggerName());
3268 
3269     filterModel.processNewLoggingEvent(event);
3270   }
3271 
3272     public void findNextMarker() {
3273       EventQueue.invokeLater(new Runnable() {
3274           public void run() {
3275               int startRow = table.getSelectedRow() + 1;
3276               int filteredEventsSize = getFilteredEvents().size();
3277               if (startRow > filteredEventsSize - 1) {
3278                   startRow = 0;
3279               }
3280               final int nextRow = tableModel.locate(findMarkerRule, startRow, true);
3281 
3282               if (nextRow > -1) {
3283                   table.scrollToRow(nextRow);
3284               }
3285           }
3286       });
3287     }
3288 
3289     public void findPreviousMarker() {
3290         EventQueue.invokeLater(new Runnable() {
3291             public void run() {
3292                 int startRow = table.getSelectedRow() - 1;
3293                 int filteredEventsSize = getFilteredEvents().size();
3294                 if (startRow < 0) {
3295                     startRow = filteredEventsSize - 1;
3296                 }
3297                 final int previousRow = tableModel.locate(findMarkerRule, startRow, false);
3298 
3299                 if (previousRow > -1) {
3300                     table.scrollToRow(previousRow);
3301                 }
3302             }
3303         });
3304     }
3305 
3306     public void clearAllMarkers() {
3307       //this will get the properties to be removed from both tables..but
3308       tableModel.removePropertyFromEvents(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE);
3309     }
3310 
3311     public void toggleMarker() {
3312         int row = table.getSelectedRow();
3313         if (row != -1) {
3314           LoggingEventWrapper loggingEventWrapper = tableModel.getRow(row);
3315           if (loggingEventWrapper != null) {
3316               Object marker = loggingEventWrapper.getLoggingEvent().getProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE);
3317               if (marker == null) {
3318                   loggingEventWrapper.setProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE, "set");
3319               } else {
3320                   loggingEventWrapper.removeProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE);
3321               }
3322               //if marker -was- null, it no longer is (may need to add the column)
3323               tableModel.fireRowUpdated(row, (marker == null));
3324           }
3325         }
3326     }
3327 
3328     public void layoutComponents()
3329     {
3330         if (preferenceModel.isDetailPaneVisible()) {
3331           showDetailPane();
3332          } else {
3333           hideDetailPane();
3334         }
3335     }
3336 
3337   public void setFindText(String findText) {
3338     findCombo.setSelectedItem(findText);
3339     findNext();
3340   }
3341 
3342   public String getFindText() {
3343     Object selectedItem = findCombo.getSelectedItem();
3344     if (selectedItem == null) {
3345       return "";
3346     }
3347     return selectedItem.toString();
3348   }
3349 
3350   /**
3351    * This class receives notification when the Refine focus or find field is
3352    * updated, where a background thread periodically wakes up and checks if
3353    * they have stopped typing yet. This ensures that the filtering of the
3354    * model is not done for every single character typed.
3355    *
3356    * @author Paul Smith psmith
3357    */
3358   private final class DelayedTextDocumentListener
3359     implements DocumentListener {
3360     private static final long CHECK_PERIOD = 1000;
3361     private final JTextField textField;
3362     private long lastTimeStamp = System.currentTimeMillis();
3363     private final Thread delayThread;
3364     private final String defaultToolTip;
3365     private String lastText = "";
3366 
3367     private DelayedTextDocumentListener(final JTextField textFeld) {
3368       super();
3369       this.textField = textFeld;
3370       this.defaultToolTip = textFeld.getToolTipText();
3371 
3372       this.delayThread =
3373         new Thread(
3374           new Runnable() {
3375             public void run() {
3376               while (true) {
3377                 try {
3378                   Thread.sleep(CHECK_PERIOD);
3379                 } catch (InterruptedException e) {
3380                 }
3381 
3382                 if (
3383                   (System.currentTimeMillis() - lastTimeStamp) < CHECK_PERIOD) {
3384                   // They typed something since the last check. we ignor
3385                   // this for a sample period
3386                   //                logger.debug("Typed something since the last check");
3387                 } else if (
3388                   (System.currentTimeMillis() - lastTimeStamp) < (2 * CHECK_PERIOD)) {
3389                   // they stopped typing recently, but have stopped for at least
3390                   // 1 sample period. lets apply the filter
3391                   //                logger.debug("Typed something recently applying filter");
3392                   if (!(textFeld.getText().trim().equals(lastText.trim()))) {
3393                     lastText = textFeld.getText();
3394                     EventQueue.invokeLater(new Runnable()
3395                     {
3396                         public void run()
3397                         {
3398                           setFilter();
3399                         }
3400                     });
3401                   }
3402                 } else {
3403                   // they stopped typing a while ago, let's forget about it
3404                   //                logger.debug(
3405                   //                  "They stoppped typing a while ago, assuming filter has been applied");
3406                 }
3407               }
3408             }
3409           });
3410 
3411       delayThread.setPriority(Thread.MIN_PRIORITY);
3412       delayThread.start();
3413     }
3414 
3415     /**
3416      * Update timestamp
3417      *
3418      * @param e
3419      */
3420     public void insertUpdate(DocumentEvent e) {
3421       notifyChange();
3422     }
3423 
3424     /**
3425      * Update timestamp
3426      *
3427      * @param e
3428      */
3429     public void removeUpdate(DocumentEvent e) {
3430       notifyChange();
3431     }
3432 
3433     /**
3434      * Update timestamp
3435      *
3436      * @param e
3437      */
3438     public void changedUpdate(DocumentEvent e) {
3439       notifyChange();
3440     }
3441 
3442     /**
3443      * Update timestamp
3444      */
3445     private void notifyChange() {
3446       this.lastTimeStamp = System.currentTimeMillis();
3447     }
3448 
3449     /**
3450      * Update refinement rule based on the entered expression.
3451      */
3452     private void setFilter() {
3453       if (textField.getText().trim().equals("")) {
3454         //reset background color in case we were previously an invalid expression
3455         textField.setBackground(UIManager.getColor("TextField.background"));
3456         tableRuleMediator.setFilterRule(null);
3457         searchRuleMediator.setFilterRule(null);
3458         textField.setToolTipText(defaultToolTip);
3459         if (findRule != null) {
3460           currentSearchMatchCount=tableModel.getSearchMatchCount();
3461           statusBar.setSearchMatchCount(currentSearchMatchCount, getIdentifier());
3462         }
3463       } else {
3464         try {
3465           tableRuleMediator.setFilterRule(ExpressionRule.getRule(textField.getText()));
3466           searchRuleMediator.setFilterRule(ExpressionRule.getRule(textField.getText()));
3467           textField.setToolTipText(defaultToolTip);
3468           if (findRule != null) {
3469             currentSearchMatchCount=tableModel.getSearchMatchCount();
3470             statusBar.setSearchMatchCount(currentSearchMatchCount, getIdentifier());
3471           }
3472           //valid expression, reset background color in case we were previously an invalid expression
3473           textField.setBackground(UIManager.getColor("TextField.background"));
3474         } catch (IllegalArgumentException iae) {
3475           //invalid expression, change background of the field
3476           textField.setToolTipText(iae.getMessage());
3477           textField.setBackground(ChainsawConstants.INVALID_EXPRESSION_BACKGROUND);
3478           if (findRule != null) {
3479             currentSearchMatchCount=tableModel.getSearchMatchCount();
3480             statusBar.setSearchMatchCount(currentSearchMatchCount, getIdentifier());
3481           }
3482         }
3483       }
3484     }
3485   }
3486 
3487   private final class TableMarkerListener extends MouseAdapter {
3488     private JTable markerTable;
3489     private EventContainer markerEventContainer;
3490     private EventContainer otherMarkerEventContainer;
3491 
3492     private TableMarkerListener(JTable markerTable, EventContainer markerEventContainer, EventContainer otherMarkerEventContainer) {
3493       this.markerTable = markerTable;
3494       this.markerEventContainer = markerEventContainer;
3495       this.otherMarkerEventContainer = otherMarkerEventContainer;
3496     }
3497     
3498       public void mouseClicked(MouseEvent evt) {
3499           if (evt.getClickCount() == 2) {
3500               int row = markerTable.rowAtPoint(evt.getPoint());
3501               if (row != -1) {
3502                 LoggingEventWrapper loggingEventWrapper = markerEventContainer.getRow(row);
3503                 if (loggingEventWrapper != null) {
3504                     Object marker = loggingEventWrapper.getLoggingEvent().getProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE);
3505                     if (marker == null) {
3506                         loggingEventWrapper.setProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE, "set");
3507                     } else {
3508                         loggingEventWrapper.removeProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE);
3509                     }
3510                     //if marker -was- null, it no longer is (may need to add the column)
3511                     markerEventContainer.fireRowUpdated(row, (marker == null));
3512                     otherMarkerEventContainer.fireRowUpdated(otherMarkerEventContainer.getRowIndex(loggingEventWrapper), (marker == null));
3513                 }
3514               }
3515           }
3516       }
3517   }
3518 
3519   /**
3520    * Update active tooltip
3521    */
3522   private final class TableColumnDetailMouseListener extends MouseMotionAdapter {
3523     private int currentRow = -1;
3524     private JTable detailTable;
3525     private EventContainer detailEventContainer;
3526 
3527     private TableColumnDetailMouseListener(JTable detailTable, EventContainer detailEventContainer) {
3528       this.detailTable = detailTable;
3529       this.detailEventContainer = detailEventContainer;
3530     }
3531 
3532     /**
3533      * Update tooltip based on mouse position
3534      *
3535      * @param evt
3536      */
3537     public void mouseMoved(MouseEvent evt) {
3538       currentPoint = evt.getPoint();
3539       currentTable = detailTable;
3540 
3541       if (preferenceModel.isToolTips()) {
3542         int row = detailTable.rowAtPoint(evt.getPoint());
3543 
3544         if ((row == currentRow) || (row == -1)) {
3545           return;
3546         }
3547 
3548         currentRow = row;
3549 
3550         LoggingEventWrapper event = detailEventContainer.getRow(currentRow);
3551 
3552         if (event != null) {
3553           String toolTipText = getToolTipTextForEvent(event);
3554           detailTable.setToolTipText(toolTipText);
3555         }
3556       } else {
3557         detailTable.setToolTipText(null);
3558       }
3559     }
3560   }
3561 
3562   //if columnmoved or columnremoved callback received, re-apply table's sort index based
3563   //sort column name
3564   private class ChainsawTableColumnModelListener implements TableColumnModelListener {
3565     private JSortTable modelListenerTable;
3566 
3567     private ChainsawTableColumnModelListener(JSortTable modelListenerTable) {
3568       this.modelListenerTable = modelListenerTable;
3569     }
3570 
3571     public void columnAdded(TableColumnModelEvent e) {
3572       //no-op
3573     }
3574 
3575     /**
3576      * Update sorted column
3577      *
3578      * @param e
3579      */
3580     public void columnRemoved(TableColumnModelEvent e) {
3581       modelListenerTable.updateSortedColumn();
3582     }
3583 
3584     /**
3585      * Update sorted column
3586      *
3587      * @param e
3588      */
3589     public void columnMoved(TableColumnModelEvent e) {
3590       modelListenerTable.updateSortedColumn();
3591     }
3592 
3593     /**
3594      * Ignore margin changed
3595      *
3596      * @param e
3597      */
3598     public void columnMarginChanged(ChangeEvent e) {
3599     }
3600 
3601     /**
3602      * Ignore selection changed
3603      *
3604      * @param e
3605      */
3606     public void columnSelectionChanged(ListSelectionEvent e) {
3607     }
3608   }
3609 
3610   /**
3611    * Thread that periodically checks if the selected row has changed, and if
3612    * it was, updates the Detail Panel with the detailed Logging information
3613    */
3614   private class DetailPaneUpdater implements PropertyChangeListener {
3615     private int selectedRow = -1;
3616     int lastRow = -1;
3617     private DetailPaneUpdater() {
3618     }
3619 
3620     /**
3621      * Update detail pane to display information about the LoggingEvent at index row
3622      *
3623      * @param row
3624      */
3625     private void setSelectedRow(int row) {
3626       selectedRow = row;
3627       updateDetailPane();
3628     }
3629 
3630     private void setAndUpdateSelectedRow(int row) {
3631         selectedRow = row;
3632         updateDetailPane(true);
3633     }
3634 
3635     private void updateDetailPane() {
3636         updateDetailPane(false);
3637     }
3638     /**
3639      * Update detail pane
3640      */
3641     private void updateDetailPane(boolean force) {
3642             /*
3643              * Don't bother doing anything if it's not visible. Note: the isVisible() method on
3644              * Component is not really accurate here because when the button to toggle display of
3645              * the detail pane is triggered it still appears as 'visible' for some reason.
3646              */
3647       if (!preferenceModel.isDetailPaneVisible()) {
3648         return;
3649       }
3650 
3651 	      LoggingEventWrapper loggingEventWrapper = null;
3652 	      if (force || (selectedRow != -1 && (lastRow != selectedRow))) {
3653 	        loggingEventWrapper = tableModel.getRow(selectedRow);
3654 	
3655 	        if (loggingEventWrapper != null) {
3656 	          final StringBuffer buf = new StringBuffer();
3657 	          buf.append(detailLayout.getHeader())
3658 	             .append(detailLayout.format(loggingEventWrapper.getLoggingEvent())).append(
3659 	            detailLayout.getFooter());
3660 	          if (buf.length() > 0) {
3661 		          	try {
3662 		          		final Document doc = detail.getEditorKit().createDefaultDocument();
3663 		          		detail.getEditorKit().read(new StringReader(buf.toString()), doc, 0);
3664 
3665 				      	SwingHelper.invokeOnEDT(new Runnable() {
3666 				      		public void run() {
3667 				      			detail.setDocument(doc);
3668                                 JTextComponentFormatter.applySystemFontAndSize(detail);
3669 				      			detail.setCaretPosition(0);
3670                                 lastRow = selectedRow;
3671 				      		}
3672 				      	});
3673 		          	} catch (Exception e) {}
3674 	      		}
3675 	        }
3676 	      }
3677 	
3678 	      if (loggingEventWrapper == null && (lastRow != selectedRow)) {
3679           	try {
3680           		final Document doc = detail.getEditorKit().createDefaultDocument();
3681           		detail.getEditorKit().read(new StringReader("<html>Nothing selected</html>"), doc, 0);
3682 		      	SwingHelper.invokeOnEDT(new Runnable() {
3683 		      		public void run() {
3684 		      			detail.setDocument(doc);
3685                         JTextComponentFormatter.applySystemFontAndSize(detail);
3686 		      			detail.setCaretPosition(0);
3687                         lastRow = selectedRow;
3688 		      		}
3689 		      	});
3690           	} catch (Exception e) {}
3691   		}
3692     }
3693 
3694     /**
3695      * Update detail pane layout if it's changed
3696      *
3697      * @param arg0
3698      */
3699     public void propertyChange(PropertyChangeEvent arg0) {
3700       SwingUtilities.invokeLater(
3701         new Runnable() {
3702           public void run() {
3703             updateDetailPane(true);
3704           }
3705         });
3706     }
3707   }
3708     private class ThrowableDisplayMouseAdapter extends MouseAdapter {
3709       private JTable throwableTable;
3710       private EventContainer throwableEventContainer;
3711       final JDialog detailDialog;
3712       final JEditorPane detailArea;
3713       public ThrowableDisplayMouseAdapter(JTable throwableTable, EventContainer throwableEventContainer) {
3714         this.throwableTable = throwableTable;
3715         this.throwableEventContainer = throwableEventContainer;
3716 
3717         detailDialog = new JDialog((JFrame) null, true);
3718         Container container = detailDialog.getContentPane();
3719         detailArea = new JEditorPane();
3720         JTextComponentFormatter.applySystemFontAndSize(detailArea);
3721         detailArea.setEditable(false);
3722         Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize();
3723         detailArea.setPreferredSize(new Dimension(screenDimension.width / 2, screenDimension.height / 2));
3724         container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
3725         container.add(new JScrollPane(detailArea));
3726 
3727         detailDialog.pack();
3728       }
3729         public void mouseClicked(MouseEvent e)
3730         {
3731             TableColumn column = throwableTable.getColumnModel().getColumn(throwableTable.columnAtPoint(e.getPoint()));
3732             if (!column.getHeaderValue().toString().toUpperCase().equals(ChainsawColumns.getColumnName(ChainsawColumns.INDEX_THROWABLE_COL_NAME))) {
3733                 return;
3734             }
3735 
3736             LoggingEventWrapper loggingEventWrapper = throwableEventContainer.getRow(throwableTable.getSelectedRow());
3737 
3738             //throwable string representation may be a length-one empty array
3739             String[] ti = loggingEventWrapper.getLoggingEvent().getThrowableStrRep();
3740             if (ti != null && ti.length > 0 && (!(ti.length == 1 && ti[0].equals("")))) {
3741                  detailDialog.setTitle(throwableTable.getColumnName(throwableTable.getSelectedColumn()) + " detail...");
3742                   StringBuffer buf = new StringBuffer();
3743                   buf.append(loggingEventWrapper.getLoggingEvent().getMessage());
3744                   buf.append("\n");
3745                   for (int i = 0; i < ti.length; i++) {
3746                     buf.append(ti[i]).append("\n    ");
3747                   }
3748 
3749                   detailArea.setText(buf.toString());
3750                   SwingHelper.invokeOnEDT(new Runnable() {
3751                     public void run() {
3752                       centerAndSetVisible(detailDialog);
3753                     }
3754                   });
3755                 }
3756         }
3757     }
3758 
3759     private class MarkerCellEditor implements TableCellEditor {
3760       JTable currentTable;
3761       JTextField textField = new JTextField();
3762       Set cellEditorListeners = new HashSet();
3763       private LoggingEventWrapper currentLoggingEventWrapper;
3764       private final Object mutex = new Object();
3765 
3766         public Object getCellEditorValue()
3767         {
3768             return textField.getText();
3769         }
3770 
3771         public boolean isCellEditable(EventObject anEvent)
3772         {
3773             return true;
3774         }
3775 
3776         public boolean shouldSelectCell(EventObject anEvent)
3777         {
3778             textField.selectAll();
3779             return true;
3780         }
3781 
3782         public boolean stopCellEditing()
3783         {
3784             if (textField.getText().trim().equals("")) {
3785                 currentLoggingEventWrapper.removeProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE);
3786             } else {
3787                 currentLoggingEventWrapper.setProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE, textField.getText());
3788             }
3789             //row should always exist in the main table if it is being edited
3790             tableModel.fireRowUpdated(tableModel.getRowIndex(currentLoggingEventWrapper), true);
3791             int index = searchModel.getRowIndex(currentLoggingEventWrapper);
3792             if (index > -1) {
3793               searchModel.fireRowUpdated(index, true);
3794             }
3795 
3796             ChangeEvent event = new ChangeEvent(currentTable);
3797             Set cellEditorListenersCopy;
3798             synchronized(mutex) {
3799                 cellEditorListenersCopy = new HashSet(cellEditorListeners);
3800             }
3801 
3802             for (Iterator iter = cellEditorListenersCopy.iterator();iter.hasNext();) {
3803                 ((CellEditorListener)iter.next()).editingStopped(event);
3804             }
3805             currentLoggingEventWrapper = null;
3806             currentTable = null;
3807 
3808             return true;
3809         }
3810 
3811         public void cancelCellEditing()
3812         {
3813             Set cellEditorListenersCopy;
3814             synchronized(mutex) {
3815                 cellEditorListenersCopy = new HashSet(cellEditorListeners);
3816             }
3817 
3818            ChangeEvent event = new ChangeEvent(currentTable);
3819            for (Iterator iter = cellEditorListenersCopy.iterator();iter.hasNext();) {
3820                ((CellEditorListener)iter.next()).editingCanceled(event);
3821            }
3822           currentLoggingEventWrapper = null;
3823           currentTable = null;
3824         }
3825 
3826         public void addCellEditorListener(CellEditorListener l)
3827         {
3828             synchronized(mutex) {
3829                 cellEditorListeners.add(l);
3830             }
3831         }
3832 
3833         public void removeCellEditorListener(CellEditorListener l)
3834         {
3835             synchronized(mutex) {
3836                 cellEditorListeners.remove(l);
3837             }
3838         }
3839 
3840         public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column)
3841         {
3842           currentTable = table;
3843           currentLoggingEventWrapper =((EventContainer) table.getModel()).getRow(row);
3844             if (currentLoggingEventWrapper != null) {
3845                 textField.setText(currentLoggingEventWrapper.getLoggingEvent().getProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE));
3846                 textField.selectAll();
3847             }
3848             else {
3849               textField.setText("");
3850             }
3851             return textField;
3852         }
3853     }
3854 
3855     private class EventTimeDeltaMatchThumbnail extends AbstractEventMatchThumbnail {
3856         public EventTimeDeltaMatchThumbnail() {
3857             super();
3858             initializeLists();
3859         }
3860 
3861         boolean primaryMatches(ThumbnailLoggingEventWrapper wrapper) {
3862             String millisDelta = wrapper.loggingEventWrapper.getLoggingEvent().getProperty(ChainsawConstants.MILLIS_DELTA_COL_NAME_LOWERCASE);
3863             if (millisDelta != null && !millisDelta.trim().equals("")) {
3864                 long millisDeltaLong = Long.parseLong(millisDelta);
3865                 //arbitrary
3866                 return millisDeltaLong >= 1000;
3867             }
3868             return false;
3869         }
3870 
3871         boolean secondaryMatches(ThumbnailLoggingEventWrapper wrapper) {
3872             //secondary is not used
3873             return false;
3874         }
3875 
3876         private void initializeLists() {
3877             secondaryList.clear();
3878             primaryList.clear();
3879 
3880             int i=0;
3881             for (Iterator iter = tableModel.getFilteredEvents().iterator();iter.hasNext();) {
3882                 LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper) iter.next();
3883                 ThumbnailLoggingEventWrapper wrapper = new ThumbnailLoggingEventWrapper(i, loggingEventWrapper);
3884                 i++;
3885                 //only add if there is a color defined
3886                 if (primaryMatches(wrapper)) {
3887                     primaryList.add(wrapper);
3888                 }
3889             }
3890             revalidate();
3891             repaint();
3892         }
3893 
3894         public void paintComponent(Graphics g) {
3895             super.paintComponent(g);
3896 
3897             int rowCount = table.getRowCount();
3898             if (rowCount == 0) {
3899                 return;
3900             }
3901             //use event pane height as reference height - max component height will be extended by event height if
3902             // last row is rendered, so subtract here
3903             int height = eventsPane.getHeight();
3904             int maxHeight = Math.min(maxEventHeight, (height / rowCount));
3905             int minHeight = Math.max(1, maxHeight);
3906             int componentHeight = height - minHeight;
3907             int eventHeight = minHeight;
3908 
3909             //draw all events
3910             for (Iterator iter = primaryList.iterator();iter.hasNext();) {
3911                 ThumbnailLoggingEventWrapper wrapper = (ThumbnailLoggingEventWrapper)iter.next();
3912                     if (primaryMatches(wrapper)) {
3913                         float ratio = (wrapper.rowNum / (float)rowCount);
3914         //                System.out.println("error - ratio: " + ratio + ", component height: " + componentHeight);
3915                         int verticalLocation = (int) (componentHeight * ratio);
3916 
3917                         int startX = 1;
3918                         int width = getWidth() - (startX * 2);
3919                         //max out at 50, min 2...
3920                         String millisDelta = wrapper.loggingEventWrapper.getLoggingEvent().getProperty(ChainsawConstants.MILLIS_DELTA_COL_NAME_LOWERCASE);
3921                         long millisDeltaLong = Long.parseLong(millisDelta);
3922                         long delta = Math.min(ChainsawConstants.MILLIS_DELTA_RENDERING_HEIGHT_MAX, Math.max(0, (long) (millisDeltaLong * ChainsawConstants.MILLIS_DELTA_RENDERING_FACTOR)));
3923                         float widthMaxMillisDeltaRenderRatio = ((float)width / ChainsawConstants.MILLIS_DELTA_RENDERING_HEIGHT_MAX);
3924                         int widthToUse = Math.max(2, (int)(delta * widthMaxMillisDeltaRenderRatio));
3925                         eventHeight = Math.min(maxEventHeight, eventHeight + 3);
3926 //                            eventHeight = maxEventHeight;
3927                         drawEvent(applicationPreferenceModel.getDeltaColor(), (verticalLocation - eventHeight + 1), eventHeight, g, startX, widthToUse);
3928     //                System.out.println("painting error - rownum: " + wrapper.rowNum + ", location: " + verticalLocation + ", height: " + eventHeight + ", component height: " + componentHeight + ", row count: " + rowCount);
3929                 }
3930             }
3931         }
3932     }
3933 
3934   //a listener receiving color updates needs to call configureColors on this class
3935     private class ColorizedEventAndSearchMatchThumbnail extends AbstractEventMatchThumbnail {
3936         public ColorizedEventAndSearchMatchThumbnail() {
3937             super();
3938             configureColors();
3939         }
3940 
3941         boolean primaryMatches(ThumbnailLoggingEventWrapper wrapper) {
3942             return !wrapper.loggingEventWrapper.getColorRuleBackground().equals(ChainsawConstants.COLOR_DEFAULT_BACKGROUND);
3943         }
3944 
3945         boolean secondaryMatches(ThumbnailLoggingEventWrapper wrapper) {
3946             return wrapper.loggingEventWrapper.isSearchMatch();
3947         }
3948 
3949         private void configureColors() {
3950             secondaryList.clear();
3951             primaryList.clear();
3952 
3953             int i=0;
3954             for (Iterator iter = tableModel.getFilteredEvents().iterator();iter.hasNext();) {
3955                 LoggingEventWrapper loggingEventWrapper = (LoggingEventWrapper) iter.next();
3956                 ThumbnailLoggingEventWrapper wrapper = new ThumbnailLoggingEventWrapper(i, loggingEventWrapper);
3957                 if (secondaryMatches(wrapper)) {
3958                     secondaryList.add(wrapper);
3959                 }
3960                 i++;
3961                 //only add if there is a color defined
3962                 if (primaryMatches(wrapper)) {
3963                     primaryList.add(wrapper);
3964                 }
3965             }
3966             revalidate();
3967             repaint();
3968         }
3969 
3970         public void paintComponent(Graphics g) {
3971             super.paintComponent(g);
3972 
3973             int rowCount = table.getRowCount();
3974             if (rowCount == 0) {
3975                 return;
3976             }
3977             //use event pane height as reference height - max component height will be extended by event height if
3978             // last row is rendered, so subtract here
3979             int height = eventsPane.getHeight();
3980             int maxHeight = Math.min(maxEventHeight, (height / rowCount));
3981             int minHeight = Math.max(1, maxHeight);
3982             int componentHeight = height - minHeight;
3983             int eventHeight = minHeight;
3984 
3985             //draw all non error/warning/marker events
3986             for (Iterator iter = primaryList.iterator();iter.hasNext();) {
3987                 ThumbnailLoggingEventWrapper wrapper = (ThumbnailLoggingEventWrapper)iter.next();
3988                 if (!wrapper.loggingEventWrapper.getColorRuleBackground().equals(ChainsawConstants.COLOR_DEFAULT_BACKGROUND)) {
3989                     if (wrapper.loggingEventWrapper.getLoggingEvent().getLevel().toInt() < Level.WARN.toInt() && wrapper.loggingEventWrapper.getLoggingEvent().getProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE) == null) {
3990                         float ratio = (wrapper.rowNum / (float)rowCount);
3991         //                System.out.println("error - ratio: " + ratio + ", component height: " + componentHeight);
3992                         int verticalLocation = (int) (componentHeight * ratio);
3993 
3994                         int startX = 1;
3995                         int width = getWidth() - (startX * 2);
3996 
3997                         drawEvent(wrapper.loggingEventWrapper.getColorRuleBackground(), verticalLocation, eventHeight, g, startX, width);
3998         //                System.out.println("painting error - rownum: " + wrapper.rowNum + ", location: " + verticalLocation + ", height: " + eventHeight + ", component height: " + componentHeight + ", row count: " + rowCount);
3999                     }
4000                 }
4001             }
4002 
4003             //draw warnings, error, fatal & markers last (full width)
4004             for (Iterator iter = primaryList.iterator();iter.hasNext();) {
4005                 ThumbnailLoggingEventWrapper wrapper = (ThumbnailLoggingEventWrapper)iter.next();
4006                 if (!wrapper.loggingEventWrapper.getColorRuleBackground().equals(ChainsawConstants.COLOR_DEFAULT_BACKGROUND)) {
4007                     if (wrapper.loggingEventWrapper.getLoggingEvent().getLevel().toInt() >= Level.WARN.toInt() || wrapper.loggingEventWrapper.getLoggingEvent().getProperty(ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE) != null) {
4008                         float ratio = (wrapper.rowNum / (float)rowCount);
4009         //                System.out.println("error - ratio: " + ratio + ", component height: " + componentHeight);
4010                         int verticalLocation = (int) (componentHeight * ratio);
4011 
4012                         int startX = 1;
4013                         int width = getWidth() - (startX * 2);
4014                         //narrow the color a bit if level is less than warn
4015                             //make warnings, errors a little taller
4016 
4017                         eventHeight = Math.min(maxEventHeight, eventHeight + 3);
4018 //                            eventHeight = maxEventHeight;
4019 
4020                         drawEvent(wrapper.loggingEventWrapper.getColorRuleBackground(), (verticalLocation - eventHeight + 1), eventHeight, g, startX, width);
4021     //                System.out.println("painting error - rownum: " + wrapper.rowNum + ", location: " + verticalLocation + ", height: " + eventHeight + ", component height: " + componentHeight + ", row count: " + rowCount);
4022                     }
4023                 }
4024             }
4025 
4026             for (Iterator iter = secondaryList.iterator();iter.hasNext();) {
4027                 ThumbnailLoggingEventWrapper wrapper = (ThumbnailLoggingEventWrapper)iter.next();
4028                 float ratio = (wrapper.rowNum / (float)rowCount);
4029 //                System.out.println("warning - ratio: " + ratio + ", component height: " + componentHeight);
4030                 int verticalLocation = (int) (componentHeight * ratio);
4031 
4032                 int startX = 1;
4033                 int width = getWidth() - (startX * 2);
4034                 width = (width / 2);
4035 
4036                 //use black for search indicator in the 'gutter'
4037                 drawEvent(Color.BLACK, verticalLocation, eventHeight, g, startX, width);
4038 //                System.out.println("painting warning - rownum: " + wrapper.rowNum + ", location: " + verticalLocation + ", height: " + eventHeight + ", component height: " + componentHeight + ", row count: " + rowCount);
4039             }
4040         }
4041     }
4042 
4043     abstract class AbstractEventMatchThumbnail extends JPanel {
4044         protected List primaryList = new ArrayList();
4045         protected List secondaryList = new ArrayList();
4046         protected final int maxEventHeight = 6;
4047 
4048         AbstractEventMatchThumbnail() {
4049             super();
4050             addMouseMotionListener(new MouseMotionAdapter() {
4051               public void mouseMoved(MouseEvent e) {
4052                 if (preferenceModel.isThumbnailBarToolTips()) {
4053                     int yPosition = e.getPoint().y;
4054                     ThumbnailLoggingEventWrapper event = getEventWrapperAtPosition(yPosition);
4055                     if (event != null) {
4056                         setToolTipText(getToolTipTextForEvent(event.loggingEventWrapper));
4057                     }
4058                 } else {
4059                     setToolTipText(null);
4060                 }
4061               }
4062             });
4063 
4064             addMouseListener(new MouseAdapter(){
4065                 public void mouseClicked(MouseEvent e)
4066                 {
4067                     int yPosition = e.getPoint().y;
4068                     ThumbnailLoggingEventWrapper event = getEventWrapperAtPosition(yPosition);
4069 //                    System.out.println("rowToSelect: " + rowToSelect + ", closestRow: " + event.loggingEvent.getProperty("log4jid"));
4070                     if (event != null) {
4071                         int id = Integer.parseInt(event.loggingEventWrapper.getLoggingEvent().getProperty("log4jid"));
4072                         setSelectedEvent(id);
4073                     }
4074                 }
4075             });
4076 
4077             tableModel.addTableModelListener(new TableModelListener(){
4078                 public void tableChanged(TableModelEvent e) {
4079                     int firstRow = e.getFirstRow();
4080                     //lastRow may be Integer.MAX_VALUE..if so, set lastRow to rowcount - 1 (so rowcount may be negative here, which will bypass for loops below)
4081                     int lastRow = Math.min(e.getLastRow(), table.getRowCount() - 1);
4082                     //clear everything if we got an event w/-1 for first or last row
4083                     if (firstRow < 0 || lastRow < 0) {
4084                         primaryList.clear();
4085                         secondaryList.clear();
4086                     }
4087 
4088 //                    System.out.println("lastRow: " + lastRow + ", first row: " + firstRow + ", original last row: " + e.getLastRow() + ", type: " + e.getType());
4089 
4090                     List displayedEvents = tableModel.getFilteredEvents();
4091                     if (e.getType() == TableModelEvent.INSERT) {
4092 //                        System.out.println("insert - current warnings: " + warnings.size() + ", errors: " + errors.size() + ", first row: " + firstRow + ", last row: " + lastRow);
4093                         for (int i=firstRow;i<lastRow;i++) {
4094                             LoggingEventWrapper event = (LoggingEventWrapper)displayedEvents.get(i);
4095                             ThumbnailLoggingEventWrapper wrapper = new ThumbnailLoggingEventWrapper(i, event);
4096                             if (secondaryMatches(wrapper)) {
4097                                 secondaryList.add(wrapper);
4098 //                                System.out.println("added warning: " + i + " - " + event.getLevel());
4099                             }
4100                             if (primaryMatches(wrapper)) {
4101                                 //add to this one
4102                                 primaryList.add(wrapper);
4103                             }
4104 //                                System.out.println("added error: " + i + " - " + event.getLevel());
4105                         }
4106 //                        System.out.println("insert- new warnings: " + warnings + ", errors: " + errors);
4107 
4108                         //run evaluation on rows & add to list
4109                     } else if (e.getType() == TableModelEvent.DELETE) {
4110                         //find each eventwrapper with an id in the deleted range and remove it...
4111 //                        System.out.println("delete- current warnings: " + warnings.size() + ", errors: " + errors.size() + ", first row: " + firstRow + ", last row: " + lastRow + ", displayed event count: " + displayedEvents.size() );
4112                         for (Iterator iter = secondaryList.iterator();iter.hasNext();) {
4113                             ThumbnailLoggingEventWrapper wrapper = (ThumbnailLoggingEventWrapper)iter.next();
4114                             if ((wrapper.rowNum >= firstRow) && (wrapper.rowNum <= lastRow)) {
4115 //                                System.out.println("deleting find: " + wrapper);
4116                                 iter.remove();
4117                             }
4118                         }
4119                         for (Iterator iter = primaryList.iterator();iter.hasNext();) {
4120                             ThumbnailLoggingEventWrapper wrapper = (ThumbnailLoggingEventWrapper)iter.next();
4121                             if ((wrapper.rowNum >= firstRow) && (wrapper.rowNum <= lastRow)) {
4122 //                                System.out.println("deleting error: " + wrapper);
4123                                 iter.remove();
4124                             }
4125                         }
4126 //                        System.out.println("delete- new warnings: " + warnings.size() + ", errors: " + errors.size());
4127 
4128                         //remove any matching rows
4129                     } else if (e.getType() == TableModelEvent.UPDATE) {
4130 //                        System.out.println("update - about to delete old warnings in range: " + firstRow + " to " + lastRow + ", current warnings: " + warnings.size() + ", errors: " + errors.size());
4131                         //find each eventwrapper with an id in the deleted range and remove it...
4132                         for (Iterator iter = secondaryList.iterator();iter.hasNext();) {
4133                             ThumbnailLoggingEventWrapper wrapper = (ThumbnailLoggingEventWrapper)iter.next();
4134                             if ((wrapper.rowNum >= firstRow) && (wrapper.rowNum <= lastRow)) {
4135 //                                System.out.println("update - deleting warning: " + wrapper);
4136                                 iter.remove();
4137                             }
4138                         }
4139                         for (Iterator iter = primaryList.iterator();iter.hasNext();) {
4140                             ThumbnailLoggingEventWrapper wrapper = (ThumbnailLoggingEventWrapper)iter.next();
4141                             if ((wrapper.rowNum >= firstRow) && (wrapper.rowNum <= lastRow)) {
4142 //                                System.out.println("update - deleting error: " + wrapper);
4143                                 iter.remove();
4144                             }
4145                         }
4146 //                        System.out.println("update - after deleting old warnings in range: " + firstRow + " to " + lastRow + ", new warnings: " + warnings.size() + ", errors: " + errors.size());
4147                         //NOTE: for update, we need to do i<= lastRow
4148                         for (int i=firstRow;i<=lastRow;i++) {
4149                             LoggingEventWrapper event = (LoggingEventWrapper)displayedEvents.get(i);
4150                             ThumbnailLoggingEventWrapper wrapper = new ThumbnailLoggingEventWrapper(i, event);
4151 //                                System.out.println("update - adding error: " + i + ", event: " + event.getMessage());
4152                             //only add event to thumbnail if there is a color
4153                             if (primaryMatches(wrapper)) {
4154                                 //!wrapper.loggingEvent.getColorRuleBackground().equals(ChainsawConstants.COLOR_DEFAULT_BACKGROUND)
4155                                 primaryList.add(wrapper);
4156                             } else {
4157                                 primaryList.remove(wrapper);
4158                             }
4159 
4160                             if (secondaryMatches(wrapper)) {
4161                                 //event.isSearchMatch())
4162 //                                System.out.println("update - adding marker: " + i + ", event: " + event.getMessage());
4163                                 secondaryList.add(wrapper);
4164                             } else {
4165                                 secondaryList.remove(wrapper);
4166                             }
4167                         }
4168 //                        System.out.println("update - new warnings: " + warnings.size() + ", errors: " + errors.size());
4169                     }
4170                     revalidate();
4171                     repaint();
4172                     //run this in an invokeLater block to ensure this action is enqueued to the end of the EDT
4173                     EventQueue.invokeLater(new Runnable() {
4174                     public void run() {
4175                       if (isScrollToBottom()) {
4176                           scrollToBottom();
4177                       }
4178                     }});
4179                 }
4180             });
4181         }
4182 
4183         abstract boolean primaryMatches(ThumbnailLoggingEventWrapper wrapper);
4184 
4185         abstract boolean secondaryMatches(ThumbnailLoggingEventWrapper wrapper);
4186         /**
4187          * Get event wrapper - may be null
4188          * @param yPosition
4189          * @return event wrapper or null
4190          */
4191         protected ThumbnailLoggingEventWrapper getEventWrapperAtPosition(int yPosition) {
4192             int rowCount = table.getRowCount();
4193 
4194             //'effective' height of this component is scrollpane height
4195             int height = eventsPane.getHeight();
4196 
4197             yPosition = Math.max(yPosition, 0);
4198 
4199             //don't let clicklocation exceed height
4200             if (yPosition >= height) {
4201                 yPosition = height;
4202             }
4203 
4204     //                    System.out.println("clicked y pos: " + e.getPoint().y + ", relative: " + clickLocation);
4205             float ratio = (float) yPosition / height;
4206             int rowToSelect = Math.round(rowCount * ratio);
4207     //                    System.out.println("rowCount: " + rowCount + ", height: " + height + ", clickLocation: " + clickLocation + ", ratio: " + ratio + ", rowToSelect: " + rowToSelect);
4208             ThumbnailLoggingEventWrapper event = getClosestRow(rowToSelect);
4209             return event;
4210         }
4211 
4212         private ThumbnailLoggingEventWrapper getClosestRow(int rowToSelect) {
4213             ThumbnailLoggingEventWrapper closestRow = null;
4214             int rowDelta = Integer.MAX_VALUE;
4215             for (Iterator iter = secondaryList.iterator();iter.hasNext();) {
4216                 ThumbnailLoggingEventWrapper event = (ThumbnailLoggingEventWrapper) iter.next();
4217                 int newRowDelta = Math.abs(rowToSelect - event.rowNum);
4218                 if (newRowDelta < rowDelta) {
4219                     closestRow = event;
4220                     rowDelta = newRowDelta;
4221                 }
4222             }
4223             for (Iterator iter = primaryList.iterator();iter.hasNext();) {
4224                 ThumbnailLoggingEventWrapper event = (ThumbnailLoggingEventWrapper) iter.next();
4225                 int newRowDelta = Math.abs(rowToSelect - event.rowNum);
4226                 if (newRowDelta < rowDelta) {
4227                     closestRow = event;
4228                     rowDelta = newRowDelta;
4229                 }
4230             }
4231             return closestRow;
4232         }
4233 
4234         public Point getToolTipLocation(MouseEvent event) {
4235             //shift tooltip down so the the pointer doesn't cover up events below the current mouse location
4236             return new Point(event.getX(), event.getY() + 30);
4237         }
4238 
4239         protected void drawEvent(Color newColor, int verticalLocation, int eventHeight, Graphics g, int x, int width) {
4240     //            System.out.println("painting: - color: " + newColor + ", verticalLocation: " + verticalLocation + ", eventHeight: " + eventHeight);
4241             //center drawing at vertical location
4242             int y = verticalLocation + (eventHeight / 2);
4243             Color oldColor = g.getColor();
4244             g.setColor(newColor);
4245             g.fillRect(x, y, width, eventHeight);
4246             if (eventHeight >= 3) {
4247                 g.setColor(newColor.darker());
4248                 g.drawRect(x, y, width, eventHeight);
4249             }
4250             g.setColor(oldColor);
4251         }
4252     }
4253 
4254     class ThumbnailLoggingEventWrapper {
4255         int rowNum;
4256         LoggingEventWrapper loggingEventWrapper;
4257         public ThumbnailLoggingEventWrapper(int rowNum, LoggingEventWrapper loggingEventWrapper) {
4258             this.rowNum = rowNum;
4259             this.loggingEventWrapper = loggingEventWrapper;
4260         }
4261 
4262         public String toString() {
4263             return "event - rownum: " + rowNum + ", level: " + loggingEventWrapper.getLoggingEvent().getLevel();
4264         }
4265 
4266         public boolean equals(Object o) {
4267             if (this == o) {
4268                 return true;
4269             }
4270             if (o == null || getClass() != o.getClass()) {
4271                 return false;
4272             }
4273 
4274             ThumbnailLoggingEventWrapper that = (ThumbnailLoggingEventWrapper) o;
4275 
4276             if (loggingEventWrapper != null ? !loggingEventWrapper.equals(that.loggingEventWrapper) : that.loggingEventWrapper != null) {
4277                 return false;
4278             }
4279 
4280             return true;
4281         }
4282 
4283         public int hashCode() {
4284             return loggingEventWrapper != null ? loggingEventWrapper.hashCode() : 0;
4285         }
4286     }
4287 
4288     class AutoFilterComboBox extends JComboBox {
4289         private boolean bypassFiltering;
4290         private List allEntries = new ArrayList();
4291         private List displayedEntries = new ArrayList();
4292         private AutoFilterComboBoxModel model = new AutoFilterComboBoxModel();
4293         //editor component
4294         private final JTextField textField = new JTextField();
4295         private String lastTextToMatch;
4296 
4297         public AutoFilterComboBox() {
4298             textField.setPreferredSize(getPreferredSize());
4299             setModel(model);
4300             setEditor(new AutoFilterEditor());
4301             ((JTextField)getEditor().getEditorComponent()).getDocument().addDocumentListener(new AutoFilterDocumentListener());
4302             setEditable(true);
4303             addPopupMenuListener(new PopupMenuListenerImpl());
4304         }
4305 
4306         public Vector getModelData() {
4307             //reverse the model order, because it will be un-reversed when we reload it from saved settings
4308             Vector vector = new Vector();
4309             for (Iterator iter = allEntries.iterator();iter.hasNext();) {
4310                 vector.insertElementAt(iter.next(), 0);
4311             }
4312             return vector;
4313         }
4314 
4315         private void refilter() {
4316             //only refilter if we're not bypassing filtering AND the text has changed since the last call to refilter
4317             String textToMatch = getEditor().getItem().toString();
4318             if (bypassFiltering || (lastTextToMatch != null && lastTextToMatch.equals(textToMatch))) {
4319                 return;
4320             }
4321             lastTextToMatch = textToMatch;
4322             bypassFiltering = true;
4323                 model.removeAllElements();
4324                 List entriesCopy = new ArrayList(allEntries);
4325                 for (Iterator iter = entriesCopy.iterator();iter.hasNext();) {
4326                     String thisEntry = iter.next().toString();
4327                     if (thisEntry.toLowerCase(Locale.ENGLISH).contains(textToMatch.toLowerCase())) {
4328                         model.addElement(thisEntry);
4329                     }
4330                 }
4331                 bypassFiltering = false;
4332                 //TODO: on no-match, don't filter at all (show the popup?)
4333                 if (displayedEntries.size() > 0 && !textToMatch.equals("")) {
4334                     showPopup();
4335                 } else {
4336                     hidePopup();
4337                 }
4338         }
4339 
4340         class AutoFilterEditor implements ComboBoxEditor {
4341             public Component getEditorComponent() {
4342                 return textField;
4343             }
4344 
4345             public void setItem(Object item) {
4346                 if (bypassFiltering) {
4347                     return;
4348                 }
4349                 bypassFiltering = true;
4350                 if (item == null) {
4351                     textField.setText("");
4352                 } else {
4353                     textField.setText(item.toString());
4354                 }
4355                 bypassFiltering = false;
4356             }
4357 
4358             public Object getItem() {
4359                 return textField.getText();
4360             }
4361 
4362             public void selectAll() {
4363                 textField.selectAll();
4364             }
4365 
4366             public void addActionListener(ActionListener listener) {
4367                 textField.addActionListener(listener);
4368             }
4369 
4370             public void removeActionListener(ActionListener listener) {
4371                 textField.removeActionListener(listener);
4372             }
4373         }
4374 
4375         class AutoFilterDocumentListener implements DocumentListener {
4376             public void insertUpdate(DocumentEvent e) {
4377                 refilter();
4378             }
4379 
4380             public void removeUpdate(DocumentEvent e) {
4381                 refilter();
4382             }
4383 
4384             public void changedUpdate(DocumentEvent e) {
4385                 refilter();
4386             }
4387         }
4388 
4389         class AutoFilterComboBoxModel extends AbstractListModel implements MutableComboBoxModel {
4390             private Object selectedItem;
4391 
4392             public void addElement(Object obj) {
4393                 //assuming add is to displayed list...add to full list (only if not a dup)
4394                 bypassFiltering = true;
4395 
4396               boolean entryExists = !allEntries.contains(obj);
4397               if (entryExists) {
4398                   allEntries.add(obj);
4399                 }
4400                 displayedEntries.add(obj);
4401                 if (!entryExists) {
4402                   fireIntervalAdded(this, displayedEntries.size() - 1, displayedEntries.size());
4403                 }
4404                 bypassFiltering = false;
4405             }
4406 
4407             public void removeElement(Object obj) {
4408                 int index = displayedEntries.indexOf(obj);
4409                 if (index != -1) {
4410                     removeElementAt(index);
4411                 }
4412             }
4413 
4414             public void insertElementAt(Object obj, int index) {
4415                 //assuming add is to displayed list...add to full list (only if not a dup)
4416                 if (allEntries.contains(obj)) {
4417                     return;
4418                 }
4419                 bypassFiltering = true;
4420                 displayedEntries.add(index, obj);
4421                 allEntries.add(index, obj);
4422                 fireIntervalAdded(this, index, index);
4423                 bypassFiltering = false;
4424                 refilter();
4425             }
4426 
4427             public void removeElementAt(int index) {
4428                 bypassFiltering = true;
4429                 //assuming removal is from displayed list..remove from full list
4430                 Object obj = displayedEntries.get(index);
4431                 allEntries.remove(obj);
4432                 displayedEntries.remove(obj);
4433                 fireIntervalRemoved(this, index, index);
4434                 bypassFiltering = false;
4435                 refilter();
4436             }
4437 
4438             public void setSelectedItem(Object item) {
4439                 if ((selectedItem != null && !selectedItem.equals(item)) || selectedItem == null && item != null) {
4440                     selectedItem = item;
4441                     fireContentsChanged(this, -1, -1);
4442                 }
4443             }
4444 
4445             public Object getSelectedItem() {
4446                 return selectedItem;
4447             }
4448 
4449             public int getSize() {
4450                 return displayedEntries.size();
4451             }
4452 
4453             public Object getElementAt(int index) {
4454                 if (index >= 0 && index < displayedEntries.size()) {
4455                     return displayedEntries.get(index);
4456                 }
4457                 return null;
4458             }
4459 
4460             public void removeAllElements() {
4461                 bypassFiltering = true;
4462                 int displayedEntrySize = displayedEntries.size();
4463                 if (displayedEntrySize > 0) {
4464                   displayedEntries.clear();
4465                   //if firecontentschaned is used, the combobox resizes..use fireintervalremoved instead, which doesn't do that..
4466                   fireIntervalRemoved(this, 0, displayedEntrySize - 1);
4467                 }
4468                 bypassFiltering = false;
4469             }
4470 
4471             public void showAllElements() {
4472               //first remove whatever is there and fire necessary events then add events
4473                 removeAllElements();
4474                 bypassFiltering = true;
4475                 displayedEntries.addAll(allEntries);
4476                 if (displayedEntries.size() > 0) {
4477                   fireIntervalAdded(this, 0, displayedEntries.size() - 1);
4478                 }
4479                 bypassFiltering = false;
4480             }
4481         }
4482 
4483         private class PopupMenuListenerImpl implements PopupMenuListener {
4484             private boolean willBecomeVisible = false;
4485 
4486             public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
4487                 bypassFiltering = true;
4488                 ((JComboBox)e.getSource()).setSelectedIndex(-1);
4489                 bypassFiltering = false;
4490                 if (!willBecomeVisible) {
4491                     //we already have a match but we're showing the popup - unfilter
4492                     if (displayedEntries.contains(textField.getText())) {
4493                         model.showAllElements();
4494                     }
4495 
4496                     //workaround for bug http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4743225
4497                     //the height of the popup after updating entries in this listener was not updated..
4498                     JComboBox list = (JComboBox) e.getSource();
4499                     willBecomeVisible = true; // the flag is needed to prevent a loop
4500                     try {
4501                         list.getUI().setPopupVisible(list, true);
4502                     } finally {
4503                         willBecomeVisible = false;
4504                     }
4505                 }
4506             }
4507 
4508             public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
4509                 //no-op
4510             }
4511 
4512             public void popupMenuCanceled(PopupMenuEvent e) {
4513                 //no-op
4514             }
4515         }
4516     }
4517 
4518   class ToggleToolTips extends JCheckBoxMenuItem {
4519     public ToggleToolTips() {
4520       super("Show ToolTips", new ImageIcon(ChainsawIcons.TOOL_TIP));
4521   addActionListener(
4522     new ActionListener() {
4523       public void actionPerformed(ActionEvent evt) {
4524         preferenceModel.setToolTips(isSelected());
4525       }
4526     });
4527     }
4528   }
4529 }