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 }