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