/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 */
package org.apache.log4j.chainsaw;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.chainsaw.color.RuleColorizer;
import org.apache.log4j.chainsaw.filter.FilterModel;
import org.apache.log4j.chainsaw.icons.ChainsawIcons;
import org.apache.log4j.chainsaw.icons.LineIconFactory;
import org.apache.log4j.rule.AbstractRule;
import org.apache.log4j.rule.ColorRule;
import org.apache.log4j.rule.ExpressionRule;
import org.apache.log4j.rule.Rule;
import org.apache.log4j.spi.LoggingEvent;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;


/**
 * A panel that encapsulates the Logger Name tree, with associated actions
 * and implements the Rule interface so that it can filter in/out events
 * that do not match the users request for refining the view based on Loggers.
 *
 * @author Paul Smith &lt;psmith@apache.org&gt;
 */
final class LoggerNameTreePanel extends JPanel implements LoggerNameListener {
    //~ Static fields/initializers ==============================================

    private static final int WARN_DEPTH = 4;

    //~ Instance fields =========================================================

    private LoggerNameTreeCellRenderer cellRenderer =
        new LoggerNameTreeCellRenderer();
    private final Action clearIgnoreListAction;
    private final Action closeAction;
    private final JButton closeButton = new SmallButton();
    private final Action collapseAction;
    private final JButton collapseButton = new SmallButton();
    private final Action editLoggerAction;
    private final JButton editLoggerButton = new SmallButton();
    private final Action expandAction;
    private final Action findAction;
    private final Action clearFindNextAction;
    private final Action defineColorRuleForLoggerAction;
    private final Action setRefineFocusAction;
    private final Action updateRefineFocusAction;
    private final Action updateFindAction;
    private final JButton expandButton = new SmallButton();
    private final Action focusOnAction;
    private final Action clearRefineFocusAction;
    private final SmallToggleButton focusOnLoggerButton =
        new SmallToggleButton();
    private final Set hiddenSet = new HashSet();
    private final Action hideAction;
    private final Action hideSubLoggersAction;
    private final LogPanelPreferenceModel preferenceModel;

    private final JList ignoreList = new JList();
    private final JEditorPane ignoreExpressionEntryField = new JEditorPane();
    private final JEditorPane alwaysDisplayExpressionEntryField = new JEditorPane();
    private final JScrollPane ignoreListScroll = new JScrollPane(ignoreList);
    private final JDialog ignoreDialog = new JDialog();
    private final JDialog ignoreExpressionDialog = new JDialog();
    private final JDialog alwaysDisplayExpressionDialog = new JDialog();
    private final JLabel ignoreSummary = new JLabel("0 hidden loggers");
    private final JLabel ignoreExpressionSummary = new JLabel("Ignore expression");
    private final JLabel alwaysDisplayExpressionSummary = new JLabel("Always displayed expression");
    private final SmallToggleButton ignoreLoggerButton = new SmallToggleButton();
    private final EventListenerList listenerList = new EventListenerList();
    private final JTree logTree;
    private final Logger logger = LogManager.getLogger(LoggerNameTreePanel.class);

    //  private final EventListenerList focusOnActionListeners =
    //    new EventListenerList();
    private final LogPanelLoggerTreeModel logTreeModel;
    private final PopupListener popupListener;
    private final LoggerTreePopupMenu popupMenu;
    private final VisibilityRuleDelegate visibilityRuleDelegate;
    private Rule colorRuleDelegate;
    private final JScrollPane scrollTree;
    private final JToolBar toolbar = new JToolBar();
    private final LogPanel logPanel;
    private final RuleColorizer colorizer;
    private Rule ignoreExpressionRule;
    private Rule alwaysDisplayExpressionRule;
    private boolean expandRootLatch = false;
    private String currentlySelectedLoggerName;

    //~ Constructors ============================================================

    /**
     * Creates a new LoggerNameTreePanel object.
     *
     * @param logTreeModel
     */
    LoggerNameTreePanel(LogPanelLoggerTreeModel logTreeModel, LogPanelPreferenceModel preferenceModel, LogPanel logPanel, RuleColorizer colorizer, FilterModel filterModel) {
        super();
        this.logTreeModel = logTreeModel;
        this.preferenceModel = preferenceModel;
        this.logPanel = logPanel;
        this.colorizer = colorizer;

        setLayout(new BorderLayout());
        ignoreExpressionEntryField.setPreferredSize(new Dimension(300, 150));
        alwaysDisplayExpressionEntryField.setPreferredSize(new Dimension(300, 150));
        alwaysDisplayExpressionSummary.setMinimumSize(new Dimension(10, alwaysDisplayExpressionSummary.getHeight()));
        ignoreExpressionSummary.setMinimumSize(new Dimension(10, ignoreExpressionSummary.getHeight()));
        ignoreSummary.setMinimumSize(new Dimension(10, ignoreSummary.getHeight()));

        JTextComponentFormatter.applySystemFontAndSize(ignoreExpressionEntryField);
        JTextComponentFormatter.applySystemFontAndSize(alwaysDisplayExpressionEntryField);

        visibilityRuleDelegate = new VisibilityRuleDelegate();
        colorRuleDelegate =
            new AbstractRule() {
                public boolean evaluate(LoggingEvent e, Map matches) {
                    boolean hiddenLogger = e.getLoggerName() != null && isHiddenLogger(e.getLoggerName());
                    boolean hiddenExpression = (ignoreExpressionRule != null && ignoreExpressionRule.evaluate(e, null));
                    boolean alwaysDisplayExpression = (alwaysDisplayExpressionRule != null && alwaysDisplayExpressionRule.evaluate(e, null));
                    boolean hidden = (!alwaysDisplayExpression) && (hiddenLogger || hiddenExpression);
                    String currentlySelectedLoggerName = getCurrentlySelectedLoggerName();

                    return !isFocusOnSelected() && !hidden && currentlySelectedLoggerName != null && !"".equals(currentlySelectedLoggerName) && (e.getLoggerName().startsWith(currentlySelectedLoggerName + ".") || e.getLoggerName().endsWith(currentlySelectedLoggerName));
                }
            };

        logTree =
            new JTree(logTreeModel) {
                public String getToolTipText(MouseEvent ev) {
                    if (ev == null) {
                        return null;
                    }

                    TreePath path = logTree.getPathForLocation(ev.getX(), ev.getY());

                    String loggerName = getLoggerName(path);

                    if (hiddenSet.contains(loggerName)) {
                        loggerName += " (you are ignoring this logger)";
                    }

                    return loggerName;
                }
            };

        ToolTipManager.sharedInstance().registerComponent(logTree);
        logTree.setCellRenderer(cellRenderer);

        //	============================================
        logTreeModel.addTreeModelListener(new TreeModelListener() {
            public void treeNodesChanged(TreeModelEvent e) {
            }

            public void treeNodesInserted(TreeModelEvent e) {
                if (!expandRootLatch) {
                    ensureRootExpanded();
                    expandRootLatch = true;
                }
            }

            public void treeNodesRemoved(TreeModelEvent e) {
            }

            public void treeStructureChanged(TreeModelEvent e) {
            }
        });

        logTree.setEditable(false);

        //	TODO decide if Multi-selection is useful, and how it would work
        TreeSelectionModel selectionModel = new DefaultTreeSelectionModel();
        selectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        logTree.setSelectionModel(selectionModel);

        logTree.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
        scrollTree = new JScrollPane(logTree);
        toolbar.setLayout(new BoxLayout(toolbar, BoxLayout.X_AXIS));

        expandAction = createExpandAction();
        findAction = createFindNextAction();
        clearFindNextAction = createClearFindNextAction();
        defineColorRuleForLoggerAction = createDefineColorRuleForLoggerAction();
        clearRefineFocusAction = createClearRefineFocusAction();
        setRefineFocusAction = createSetRefineFocusAction();
        updateRefineFocusAction = createUpdateRefineFocusAction();
        updateFindAction = createUpdateFindAction();
        editLoggerAction = createEditLoggerAction();
        closeAction = createCloseAction();
        collapseAction = createCollapseAction();
        focusOnAction = createFocusOnAction();
        hideAction = createIgnoreAction();
        hideSubLoggersAction = createIgnoreAllAction();
        clearIgnoreListAction = createClearIgnoreListAction();

        popupMenu = new LoggerTreePopupMenu();
        popupListener = new PopupListener(popupMenu);

        setupListeners();
        configureToolbarPanel();

        add(toolbar, BorderLayout.NORTH);
        add(scrollTree, BorderLayout.CENTER);

        ignoreDialog.setTitle("Hidden/Ignored Loggers");
        ignoreDialog.setModal(true);

        ignoreExpressionDialog.setTitle("Hidden/Ignored Expression");
        ignoreExpressionDialog.setModal(true);

        alwaysDisplayExpressionDialog.setTitle("Always displayed Expression");
        alwaysDisplayExpressionDialog.setModal(true);

        JPanel ignorePanel = new JPanel();
        ignorePanel.setLayout(new BoxLayout(ignorePanel, BoxLayout.Y_AXIS));
        JPanel ignoreSummaryPanel = new JPanel();
        ignoreSummaryPanel.setLayout(new BoxLayout(ignoreSummaryPanel, BoxLayout.X_AXIS));
        ignoreSummaryPanel.add(ignoreSummary);

        Action showIgnoreDialogAction = new AbstractAction("...") {
            public void actionPerformed(ActionEvent e) {
                LogPanel.centerAndSetVisible(ignoreDialog);
            }
        };

        Action showIgnoreExpressionDialogAction = new AbstractAction("...") {
            public void actionPerformed(ActionEvent e) {
                LogPanel.centerAndSetVisible(ignoreExpressionDialog);
            }
        };


        Action showAlwaysDisplayExpressionDialogAction = new AbstractAction("...") {
            public void actionPerformed(ActionEvent e) {
                LogPanel.centerAndSetVisible(alwaysDisplayExpressionDialog);
            }
        };

        showIgnoreDialogAction.putValue(Action.SHORT_DESCRIPTION, "Click to view and manage your hidden/ignored loggers");
        JButton btnShowIgnoreDialog = new SmallButton(showIgnoreDialogAction);

        ignoreSummaryPanel.add(btnShowIgnoreDialog);
        ignorePanel.add(ignoreSummaryPanel);

        JPanel ignoreExpressionPanel = new JPanel();
        ignoreExpressionPanel.setLayout(new BoxLayout(ignoreExpressionPanel, BoxLayout.X_AXIS));
        ignoreExpressionPanel.add(ignoreExpressionSummary);
        showIgnoreExpressionDialogAction.putValue(Action.SHORT_DESCRIPTION, "Click to view and manage your hidden/ignored expression");
        JButton btnShowIgnoreExpressionDialog = new SmallButton(showIgnoreExpressionDialogAction);
        ignoreExpressionPanel.add(btnShowIgnoreExpressionDialog);

        ignorePanel.add(ignoreExpressionPanel);

        JPanel alwaysDisplayExpressionPanel = new JPanel();
        alwaysDisplayExpressionPanel.setLayout(new BoxLayout(alwaysDisplayExpressionPanel, BoxLayout.X_AXIS));
        alwaysDisplayExpressionPanel.add(alwaysDisplayExpressionSummary);
        showAlwaysDisplayExpressionDialogAction.putValue(Action.SHORT_DESCRIPTION, "Click to view and manage your always-displayed expression");
        JButton btnShowAlwaysDisplayExpressionDialog = new SmallButton(showAlwaysDisplayExpressionDialogAction);
        alwaysDisplayExpressionPanel.add(btnShowAlwaysDisplayExpressionDialog);

        ignorePanel.add(alwaysDisplayExpressionPanel);

        add(ignorePanel, BorderLayout.SOUTH);

        ignoreList.setModel(new DefaultListModel());
        ignoreList.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                if (
                    (e.getClickCount() > 1)
                        && ((e.getModifiers() & InputEvent.BUTTON1_MASK) > 0)) {
                    int index = ignoreList.locationToIndex(e.getPoint());

                    if (index >= 0) {
                        String string =
                            ignoreList.getModel().getElementAt(index).toString();
                        toggleHiddenLogger(string);
                        fireChangeEvent();

                        /**
                         * TODO this needs to get the node that has this logger and fire a visual update
                         */
                        LoggerNameTreePanel.this.logTreeModel.nodeStructureChanged(
                            (TreeNode) LoggerNameTreePanel.this.logTreeModel.getRoot());
                    }
                }
            }
        });

        JPanel ignoreListPanel = new JPanel(new BorderLayout());
        ignoreListScroll.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Double click an entry to unhide it"));
        ignoreListPanel.add(ignoreListScroll, BorderLayout.CENTER);

        JPanel ignoreExpressionDialogPanel = new JPanel(new BorderLayout());
        ignoreExpressionEntryField.addKeyListener(new ExpressionRuleContext(filterModel, ignoreExpressionEntryField));

        ignoreExpressionDialogPanel.add(new JScrollPane(ignoreExpressionEntryField), BorderLayout.CENTER);
        JButton ignoreExpressionCloseButton = new JButton(new AbstractAction(" Close ") {
            public void actionPerformed(ActionEvent e) {
                String ignoreText = ignoreExpressionEntryField.getText();

                if (updateIgnoreExpression(ignoreText)) {
                    ignoreExpressionDialog.setVisible(false);
                }
            }
        });


        JPanel alwaysDisplayExpressionDialogPanel = new JPanel(new BorderLayout());
        alwaysDisplayExpressionEntryField.addKeyListener(new ExpressionRuleContext(filterModel, alwaysDisplayExpressionEntryField));

        alwaysDisplayExpressionDialogPanel.add(new JScrollPane(alwaysDisplayExpressionEntryField), BorderLayout.CENTER);
        JButton alwaysDisplayExpressionCloseButton = new JButton(new AbstractAction(" Close ") {
            public void actionPerformed(ActionEvent e) {
                String alwaysDisplayText = alwaysDisplayExpressionEntryField.getText();

                if (updateAlwaysDisplayExpression(alwaysDisplayText)) {
                    alwaysDisplayExpressionDialog.setVisible(false);
                }
            }
        });

        JPanel closeAlwaysDisplayExpressionPanel = new JPanel();
        closeAlwaysDisplayExpressionPanel.add(alwaysDisplayExpressionCloseButton);
        alwaysDisplayExpressionDialogPanel.add(closeAlwaysDisplayExpressionPanel, BorderLayout.SOUTH);

        JPanel closeIgnoreExpressionPanel = new JPanel();
        closeIgnoreExpressionPanel.add(ignoreExpressionCloseButton);
        ignoreExpressionDialogPanel.add(closeIgnoreExpressionPanel, BorderLayout.SOUTH);

        Box ignoreListButtonPanel = Box.createHorizontalBox();

        JButton unhideAll = new JButton(new AbstractAction(" Unhide All ") {

            public void actionPerformed(final ActionEvent e) {
                SwingUtilities.invokeLater(() -> clearIgnoreListAction.actionPerformed(e));

            }
        });
        ignoreListButtonPanel.add(unhideAll);

        ignoreListButtonPanel.add(Box.createHorizontalGlue());
        JButton ignoreCloseButton = new JButton(new AbstractAction(" Close ") {

            public void actionPerformed(ActionEvent e) {
                ignoreDialog.setVisible(false);

            }
        });
        ignoreListButtonPanel.add(ignoreCloseButton);


        ignoreListPanel.add(ignoreListButtonPanel, BorderLayout.SOUTH);


        ignoreDialog.getContentPane().add(ignoreListPanel);
        ignoreDialog.pack();

        ignoreExpressionDialog.getContentPane().add(ignoreExpressionDialogPanel);
        ignoreExpressionDialog.pack();

        alwaysDisplayExpressionDialog.getContentPane().add(alwaysDisplayExpressionDialogPanel);
        alwaysDisplayExpressionDialog.pack();
    }

    private boolean updateIgnoreExpression(String ignoreText) {
        try {
            if (ignoreText != null && !ignoreText.trim().equals("")) {
                ignoreExpressionRule = ExpressionRule.getRule(ignoreText);
            } else {
                ignoreExpressionRule = null;
            }
            visibilityRuleDelegate.firePropertyChange("hiddenSet", null, null);

            updateDisplay();
            ignoreExpressionEntryField.setBackground(UIManager.getColor("TextField.background"));
            return true;
        } catch (IllegalArgumentException iae) {
            ignoreExpressionEntryField.setToolTipText(iae.getMessage());
            ignoreExpressionEntryField.setBackground(ChainsawConstants.INVALID_EXPRESSION_BACKGROUND);
            return false;
        }
    }

    private boolean updateAlwaysDisplayExpression(String alwaysDisplayText) {
        try {
            if (alwaysDisplayText != null && !alwaysDisplayText.trim().equals("")) {
                alwaysDisplayExpressionRule = ExpressionRule.getRule(alwaysDisplayText);
            } else {
                alwaysDisplayExpressionRule = null;
            }
            visibilityRuleDelegate.firePropertyChange("alwaysDisplayedSet", null, null);

            updateDisplay();
            alwaysDisplayExpressionEntryField.setBackground(UIManager.getColor("TextField.background"));
            return true;
        } catch (IllegalArgumentException iae) {
            alwaysDisplayExpressionEntryField.setToolTipText(iae.getMessage());
            alwaysDisplayExpressionEntryField.setBackground(ChainsawConstants.INVALID_EXPRESSION_BACKGROUND);
            return false;
        }
    }

    //~ Methods =================================================================

    /**
     * Adds a change Listener to this LoggerNameTreePanel to be notfied
     * when the State of the Focus or Hidden details have changed.
     *
     * @param l
     */
    public void addChangeListener(ChangeListener l) {
        listenerList.add(ChangeListener.class, l);
    }

    public Rule getLoggerColorRule() {
        return colorRuleDelegate;
    }

    public Rule getLoggerVisibilityRule() {
        return visibilityRuleDelegate;
    }

    /**
     * DOCUMENT ME!
     *
     * @param l DOCUMENT ME!
     */
    public void removeChangeListener(ChangeListener l) {
        listenerList.remove(ChangeListener.class, l);
    }

    /**
     * Ensures the Focus is set to a specific logger name
     *
     * @param
     */
    public void setFocusOn(String newLogger) {
        DefaultMutableTreeNode node = logTreeModel.lookupLogger(newLogger);

        if (node != null) {
            TreeNode[] nodes = node.getPath();
            TreePath treePath = new TreePath(nodes);
            logTree.setSelectionPath(treePath);

            if (!focusOnLoggerButton.isSelected()) {
                focusOnLoggerButton.doClick();
            }
        } else {
            logger.error("failed to lookup logger " + newLogger);
        }
    }

    private boolean isHiddenLogger(String loggerName) {
        for (Object aHiddenSet : hiddenSet) {
            String hiddenLoggerEntry = aHiddenSet.toString();
            if (loggerName.startsWith(hiddenLoggerEntry + ".") || loggerName.endsWith(hiddenLoggerEntry)) {
                return true;
            }
        }
        return false;
    }


    /**
     * DOCUMENT ME!
     *
     * @param logger
     */
    protected void toggleHiddenLogger(String logger) {
        if (!hiddenSet.contains(logger)) {
            hiddenSet.add(logger);
        } else {
            hiddenSet.remove(logger);
        }

        visibilityRuleDelegate.firePropertyChange("hiddenSet", null, null);
    }

    /**
     * Returns the full name of the Logger that is represented by
     * the currently selected Logger node in the tree.
     * <p>
     * This is the dotted name, of the current node including all it's parents.
     * <p>
     * If multiple Nodes are selected, the first path is used
     *
     * @return Logger Name or null if nothing selected
     */
    String getCurrentlySelectedLoggerName() {
        TreePath[] paths = logTree.getSelectionPaths();

        if ((paths == null) || (paths.length == 0)) {
            return null;
        }

        TreePath firstPath = paths[0];

        return getLoggerName(firstPath);
    }

    /**
     * Returns the full
     *
     * @param path DOCUMENT ME!
     * @return
     */
    String getLoggerName(TreePath path) {
        if (path != null) {
            Object[] objects = path.getPath();
            StringBuilder buf = new StringBuilder();

            for (int i = 1; i < objects.length; i++) {
                buf.append(objects[i].toString());

                if (i < (objects.length - 1)) {
                    buf.append(".");
                }
            }

            return buf.toString();
        }

        return null;
    }

    /**
     * adds a Collection of Strings to the ignore List and notifise all listeners of
     * both the "hiddenSet" property and those expecting the Rule to change
     * via the ChangeListener interface
     *
     * @param fqnLoggersToIgnore
     */
    void ignore(Collection fqnLoggersToIgnore) {
        hiddenSet.addAll(fqnLoggersToIgnore);
        visibilityRuleDelegate.firePropertyChange("hiddenSet", null, null);
        fireChangeEvent();
    }

    /**
     * Returns true if the FocusOn element has been selected
     *
     * @return true if the FocusOn action/lement has been selected
     */
    boolean isFocusOnSelected() {
        return focusOnAction.getValue("checked") != null;
    }

    void setFocusOnSelected(boolean selected) {
        if (selected) {
            focusOnAction.putValue("checked", Boolean.TRUE);
        } else {
            focusOnAction.putValue("checked", null);
        }
    }

    /**
     * Given the currently selected nodes
     * collapses all the children of those nodes.
     */
    private void collapseCurrentlySelectedNode() {
        TreePath[] paths = logTree.getSelectionPaths();

        if (paths == null) {
            return;
        }

        logger.debug("Collapsing all children of selected node");

        for (TreePath path : paths) {
            DefaultMutableTreeNode node =
                (DefaultMutableTreeNode) path.getLastPathComponent();
            Enumeration enumeration = node.depthFirstEnumeration();

            while (enumeration.hasMoreElements()) {
                DefaultMutableTreeNode child =
                    (DefaultMutableTreeNode) enumeration.nextElement();

                if ((child.getParent() != null) && (child != node)) {
                    TreeNode[] nodes =
                        ((DefaultMutableTreeNode) child.getParent()).getPath();

                    TreePath treePath = new TreePath(nodes);
                    logTree.collapsePath(treePath);
                }
            }
        }

        ensureRootExpanded();
    }

    /**
     * configures all the components that are used in the mini-toolbar of this
     * component
     */
    private void configureToolbarPanel() {
        toolbar.setFloatable(false);

        expandButton.setAction(expandAction);
        expandButton.setText(null);
        collapseButton.setAction(collapseAction);
        collapseButton.setText(null);
        focusOnLoggerButton.setAction(focusOnAction);
        focusOnLoggerButton.setText(null);
        ignoreLoggerButton.setAction(hideAction);
        ignoreLoggerButton.setText(null);

        expandButton.setFont(expandButton.getFont().deriveFont(Font.BOLD));
        collapseButton.setFont(collapseButton.getFont().deriveFont(Font.BOLD));

        editLoggerButton.setAction(editLoggerAction);
        editLoggerButton.setText(null);
        closeButton.setAction(closeAction);
        closeButton.setText(null);

        toolbar.add(expandButton);
        toolbar.add(collapseButton);
        toolbar.addSeparator();
        toolbar.add(focusOnLoggerButton);
        toolbar.add(ignoreLoggerButton);

        //    toolbar.add(editLoggerButton);
        toolbar.addSeparator();

        toolbar.add(Box.createHorizontalGlue());
        toolbar.add(closeButton);
        toolbar.add(Box.createHorizontalStrut(5));
    }

    /**
     * DOCUMENT ME!
     *
     * @return
     */
    private Action createClearIgnoreListAction() {
        Action action = new AbstractAction("Clear Ignore list", null) {
            public void actionPerformed(ActionEvent e) {
                ignoreLoggerButton.setSelected(false);
                logTreeModel.reload();
                hiddenSet.clear();
                fireChangeEvent();
            }
        };

        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Removes all entries from the Ignore list so you can see their events in the view");

        return action;
    }

    /**
     * An action that closes (hides) this panel
     *
     * @return
     */
    private Action createCloseAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                preferenceModel.setLogTreePanelVisible(false);
            }
        };

        action.putValue(Action.NAME, "Close");
        action.putValue(Action.SHORT_DESCRIPTION, "Closes the Logger panel");
        action.putValue(Action.SMALL_ICON, LineIconFactory.createCloseIcon());

        return action;
    }

    /**
     * DOCUMENT ME!
     *
     * @return
     */
    private Action createCollapseAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                collapseCurrentlySelectedNode();
            }
        };

        action.putValue(Action.SMALL_ICON, LineIconFactory.createCollapseIcon());
        action.putValue(Action.NAME, "Collapse Branch");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Collapses all the children of the currently selected node");
        action.setEnabled(false);

        return action;
    }

    private Action createEditLoggerAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                // TODO Auto-generated method stub
            }
        };

        //    TODO enable this when it's ready.
        action.putValue("enabled", Boolean.FALSE);

        action.putValue(Action.NAME, "Edit filters/colors");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Allows you to specify filters and coloring for this Logger");
        action.putValue(
            Action.SMALL_ICON, new ImageIcon(ChainsawIcons.ICON_EDIT_RECEIVER));
        action.setEnabled(false);

        return action;
    }

    /**
     * Creates an action that is used to expand the selected node
     * and all children
     *
     * @return an Action
     */
    private Action createExpandAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                expandCurrentlySelectedNode();
            }
        };

        action.putValue(Action.SMALL_ICON, LineIconFactory.createExpandIcon());
        action.putValue(Action.NAME, "Expand branch");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Expands all the child nodes of the currently selected node, recursively");
        action.setEnabled(false);

        return action;
    }

    /**
     * Creates an action that is used to find the next match of the selected node (similar to default selection behavior
     * except the search field is populated and the next match is selected.
     *
     * @return an Action
     */
    private Action createFindNextAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                findNextUsingCurrentlySelectedNode();
            }
        };

        action.putValue(Action.NAME, "Find next");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Find using the selected node");
        action.setEnabled(false);

        return action;
    }

    private Action createSetRefineFocusAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                setRefineFocusUsingCurrentlySelectedNode();
            }
        };

        action.putValue(Action.NAME, "Set 'refine focus' to selected logger");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Refine focus on the selected node");
        action.setEnabled(false);

        return action;
    }

    private Action createUpdateRefineFocusAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                updateRefineFocusUsingCurrentlySelectedNode();
            }
        };

        action.putValue(Action.NAME, "Update 'refine focus' to include selected logger");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Add selected node to 'refine focus' field");
        action.setEnabled(false);

        return action;
    }

    private Action createUpdateFindAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                updateFindUsingCurrentlySelectedNode();
            }
        };

        action.putValue(Action.NAME, "Update 'find' to include selected logger");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Add selected node to 'find' field");
        action.setEnabled(false);

        return action;
    }

    private void updateFindUsingCurrentlySelectedNode() {
        String selectedLogger = getCurrentlySelectedLoggerName();
        TreePath[] paths = logTree.getSelectionPaths();

        if (paths == null) {
            return;
        }
        String currentFindText = logPanel.getFindText();
        logPanel.setFindText(currentFindText + " || logger ~= " + selectedLogger);
    }

    private void updateRefineFocusUsingCurrentlySelectedNode() {
        String selectedLogger = getCurrentlySelectedLoggerName();
        TreePath[] paths = logTree.getSelectionPaths();

        if (paths == null) {
            return;
        }
        String currentFilterText = logPanel.getRefineFocusText();
        logPanel.setRefineFocusText(currentFilterText + " || logger ~= " + selectedLogger);
    }

    private void setRefineFocusUsingCurrentlySelectedNode() {
        String selectedLogger = getCurrentlySelectedLoggerName();
        TreePath[] paths = logTree.getSelectionPaths();

        if (paths == null) {
            return;
        }
        logPanel.setRefineFocusText("logger ~= " + selectedLogger);
    }

    private Action createDefineColorRuleForLoggerAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                String selectedLogger = getCurrentlySelectedLoggerName();
                TreePath[] paths = logTree.getSelectionPaths();

                if (paths == null) {
                    return;
                }
                Color c = JColorChooser.showDialog(getRootPane(), "Choose a color", Color.red);
                if (c != null) {
                    String expression = "logger like '^" + selectedLogger + ".*'";
                    colorizer.addRule(ChainsawConstants.DEFAULT_COLOR_RULE_NAME, new ColorRule(expression,
                        ExpressionRule.getRule(expression), c, ChainsawConstants.COLOR_DEFAULT_FOREGROUND));
                }
            }
        };

        action.putValue(Action.NAME, "Define color rule for selected logger");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Define color rule for logger");
        action.setEnabled(false);
        return action;
    }

    /**
     * Creates an action that is used to find the next match of the selected node (similar to default selection behavior
     * except the search field is populated and the next match is selected.
     *
     * @return an Action
     */
    private Action createClearFindNextAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                clearFindNext();
            }
        };

        action.putValue(Action.NAME, "Clear find field");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Clear the find field");
        action.setEnabled(false);

        return action;
    }

    private Action createClearRefineFocusAction() {
        Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                clearRefineFocus();
            }
        };

        action.putValue(Action.NAME, "Clear 'refine focus' field");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Clear the refine focus field");
        action.setEnabled(false);

        return action;
    }

    /**
     * DOCUMENT ME!
     *
     * @return
     */
    private Action createFocusOnAction() {
        final Action action = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                toggleFocusOnState();
            }
        };

        action.putValue(Action.NAME, "Focus");
        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Allows you to Focus on the selected logger by setting a filter that discards all but this Logger");
        action.putValue(
            Action.SMALL_ICON, new ImageIcon(ChainsawIcons.WINDOW_ICON));

        action.setEnabled(false);

        return action;
    }

    /**
     * DOCUMENT ME!
     *
     * @return
     */
    private Action createIgnoreAllAction() {
        Action action =
            new AbstractAction(
                "Ignore loggers below selection") {
                public void actionPerformed(ActionEvent e) {
                    //add all top level loggers as hidden loggers
                    TreePath[] paths = logTree.getSelectionPaths();

                    StringBuilder parentPathString = new StringBuilder();
                    DefaultMutableTreeNode root;
                    if ((paths == null) || (paths.length == 0)) {
                        root = (DefaultMutableTreeNode) logTreeModel.getRoot();
                    } else {
                        root = (DefaultMutableTreeNode) logTree.getSelectionPath().getLastPathComponent();
                        TreeNode[] path = root.getPath();
                        //don't add 'root logger' to path string
                        for (int i = 1; i < path.length; i++) {
                            if (i > 1) {
                                parentPathString.append(".");
                            }
                            parentPathString.append(path[i].toString());
                        }
                        if (!(parentPathString.toString().equals(""))) {
                            parentPathString.append(".");
                        }
                    }
                    Enumeration topLevelLoggersEnumeration = root.children();
                    Set topLevelLoggersSet = new HashSet();
                    while (topLevelLoggersEnumeration.hasMoreElements()) {
                        String thisLogger = topLevelLoggersEnumeration.nextElement().toString();
                        topLevelLoggersSet.add(parentPathString + thisLogger);
                    }
                    if (topLevelLoggersSet.size() > 0) {
                        ignore(topLevelLoggersSet);
                    }

                    logTreeModel.nodeChanged(root);
                    ignoreLoggerButton.setSelected(false);
                    focusOnAction.setEnabled(false);
                    popupMenu.hideCheck.setSelected(false);
                    fireChangeEvent();
                }
            };

        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Adds all loggers to your Ignore list (unhide loggers you want to see in the view)");

        return action;
    }

    /**
     * DOCUMENT ME!
     *
     * @return
     */
    private Action createIgnoreAction() {
        Action action =
            new AbstractAction(
                "Ignore this Logger", new ImageIcon(ChainsawIcons.ICON_COLLAPSE)) {
                public void actionPerformed(ActionEvent e) {
                    String logger = getCurrentlySelectedLoggerName();

                    if (logger != null) {
                        toggleHiddenLogger(logger);
                        logTreeModel.nodeChanged(
                            (TreeNode) logTree.getSelectionPath().getLastPathComponent());
                        ignoreLoggerButton.setSelected(hiddenSet.contains(logger));
                        focusOnAction.setEnabled(!hiddenSet.contains(logger));
                        popupMenu.hideCheck.setSelected(hiddenSet.contains(logger));
                    }

                    fireChangeEvent();
                }
            };

        action.putValue(
            Action.SHORT_DESCRIPTION,
            "Adds the selected Logger to your Ignore list, filtering those events from view");

        return action;
    }

    private void ensureRootExpanded() {
        logger.debug("Ensuring Root node is expanded.");

        final DefaultMutableTreeNode root =
            (DefaultMutableTreeNode) logTreeModel.getRoot();
        SwingUtilities.invokeLater(() -> logTree.expandPath(new TreePath(root)));
    }

    private void findNextUsingCurrentlySelectedNode() {
        String selectedLogger = getCurrentlySelectedLoggerName();
        TreePath[] paths = logTree.getSelectionPaths();

        if (paths == null) {
            return;
        }
        logPanel.setFindText("logger like '^" + selectedLogger + ".*'");
    }

    private void clearFindNext() {
        logPanel.setFindText("");
    }

    private void clearRefineFocus() {
        logPanel.setRefineFocusText("");
    }

    /**
     * Expands the currently selected node (if any)
     * including all the children.
     */
    private void expandCurrentlySelectedNode() {
        TreePath[] paths = logTree.getSelectionPaths();

        if (paths == null) {
            return;
        }

        logger.debug("Expanding all children of selected node");

        for (TreePath path : paths) {
            /**
             * TODO this is commented out, right now it expands all nodes including the root, so if there is a large tree..... look out.
             */

            //      /**
            //       * Handle an expansion of the Root node by only doing the first level.
            //       * Safe...
            //       */
            //      if (path.getPathCount() == 1) {
            //        logTree.expandPath(path);
            //
            //        return;
            //      }

            DefaultMutableTreeNode treeNode =
                (DefaultMutableTreeNode) path.getLastPathComponent();

            Enumeration depthEnum = treeNode.depthFirstEnumeration();

            if (!depthEnum.hasMoreElements()) {
                break;
            }

            List<Integer> depths = new ArrayList<>();

            while (depthEnum.hasMoreElements()) {
                depths.add(
                    ((DefaultMutableTreeNode) depthEnum.nextElement()).getDepth());
            }

            Collections.sort(depths);
            Collections.reverse(depths);

            int maxDepth = depths.get(0);

            if (maxDepth > WARN_DEPTH) {
                logger.warn("Should warn user, depth=" + maxDepth);
            }

            depthEnum = treeNode.depthFirstEnumeration();

            while (depthEnum.hasMoreElements()) {
                DefaultMutableTreeNode node =
                    (DefaultMutableTreeNode) depthEnum.nextElement();

                if (node.isLeaf() && node.getParent() != null) {
                    TreeNode[] nodes =
                        ((DefaultMutableTreeNode) node.getParent()).getPath();
                    TreePath treePath = new TreePath(nodes);

                    logger.debug("Expanding path:" + treePath);

                    logTree.expandPath(treePath);
                }
            }
        }
    }

    private void fireChangeEvent() {
        ChangeListener[] listeners =
            listenerList.getListeners(ChangeListener.class);
        ChangeEvent e = null;

        for (ChangeListener listener : listeners) {
            if (e == null) {
                e = new ChangeEvent(this);
            }

            listener.stateChanged(e);
        }
    }

    private void reconfigureMenuText() {
        String logger = getCurrentlySelectedLoggerName();

        if ((logger == null) || (logger.length() == 0)) {
            focusOnAction.putValue(Action.NAME, "Focus On...");
            hideAction.putValue(Action.NAME, "Ignore...");
            findAction.putValue(Action.NAME, "Find...");
            setRefineFocusAction.putValue(Action.NAME, "Set refine focus field");
            updateRefineFocusAction.putValue(Action.NAME, "Add to refine focus field");
            updateFindAction.putValue(Action.NAME, "Add to find field");
            defineColorRuleForLoggerAction.putValue(Action.NAME, "Define color rule");
        } else {
            focusOnAction.putValue(Action.NAME, "Focus On '" + logger + "'");
            hideAction.putValue(Action.NAME, "Ignore '" + logger + "'");
            findAction.putValue(Action.NAME, "Find '" + logger + "'");
            setRefineFocusAction.putValue(Action.NAME, "Set refine focus field to '" + logger + "'");
            updateRefineFocusAction.putValue(Action.NAME, "Add '" + logger + "' to 'refine focus' field");
            updateFindAction.putValue(Action.NAME, "Add '" + logger + "' to 'find' field");
            defineColorRuleForLoggerAction.putValue(Action.NAME, "Define color rule for '" + logger + "'");
        }

        // need to ensure the button doens't update itself with the text, looks stupid otherwise
        hideSubLoggersAction.putValue(Action.NAME, "Ignore loggers below selection");
        focusOnLoggerButton.setText(null);
        ignoreLoggerButton.setText(null);
    }

    /**
     * Configures varoius listeners etc for the components within
     * this Class.
     */
    private void setupListeners() {
        logTree.addMouseMotionListener(new MouseKeyIconListener());

        /**
         * Enable the actions depending on state of the tree selection
         */
        logTree.addTreeSelectionListener(e -> {
            TreePath path = e.getNewLeadSelectionPath();
            TreeNode node = null;

            if (path != null) {
                node = (TreeNode) path.getLastPathComponent();
            }
            boolean focusOnSelected = isFocusOnSelected();
            //          editLoggerAction.setEnabled(path != null);
            currentlySelectedLoggerName = getCurrentlySelectedLoggerName();
            focusOnAction.setEnabled(
                (path != null) && (node != null) && (node.getParent() != null)
                    && !hiddenSet.contains(currentlySelectedLoggerName));
            hideAction.setEnabled(
                (path != null) && (node != null) && (node.getParent() != null));
            if (!focusOnAction.isEnabled()) {
                setFocusOnSelected(false);
            } else {
            }

            expandAction.setEnabled(path != null);
            findAction.setEnabled(path != null);
            clearFindNextAction.setEnabled(true);
            defineColorRuleForLoggerAction.setEnabled(path != null);
            setRefineFocusAction.setEnabled(path != null);
            updateRefineFocusAction.setEnabled(path != null);
            updateFindAction.setEnabled(path != null);
            clearRefineFocusAction.setEnabled(true);

            if (currentlySelectedLoggerName != null) {
                boolean isHidden = hiddenSet.contains(currentlySelectedLoggerName);
                popupMenu.hideCheck.setSelected(isHidden);
                ignoreLoggerButton.setSelected(isHidden);
            }

            collapseAction.setEnabled(path != null);

            reconfigureMenuText();
            if (isFocusOnSelected()) {
                fireChangeEvent();
            }
            //fire change event if we toggled focus off
            if (focusOnSelected && !isFocusOnSelected()) {
                fireChangeEvent();
            }
            //trigger a table repaint
            logPanel.repaint();
        });

        logTree.addMouseListener(popupListener);

        /**
         * This listener ensures the Tool bar toggle button and popup menu check box
         * stay in sync, plus notifies all the ChangeListeners that
         * an effective filter criteria has been modified
         */
        focusOnAction.addPropertyChangeListener(evt -> {
            popupMenu.focusOnCheck.setSelected(isFocusOnSelected());
            focusOnLoggerButton.setSelected(isFocusOnSelected());

            if (logTree.getSelectionPath() != null) {
                logTreeModel.nodeChanged(
                    (TreeNode) logTree.getSelectionPath().getLastPathComponent());
            }
        });

        hideAction.addPropertyChangeListener(evt -> {
            if (logTree.getSelectionPath() != null) {
                logTreeModel.nodeChanged(
                    (TreeNode) logTree.getSelectionPath().getLastPathComponent());
            }
        });

        //    /**
        //     * Now add a MouseListener that fires the expansion
        //     * action if CTRL + DBL CLICK is done.
        //     */
        //    logTree.addMouseListener(
        //      new MouseAdapter() {
        //        public void mouseClicked(MouseEvent e) {
        //          if (
        //            (e.getClickCount() > 1)
        //              && ((e.getModifiers() & InputEvent.CTRL_MASK) > 0)
        //              && ((e.getModifiers() & InputEvent.BUTTON1_MASK) > 0)) {
        //            expandCurrentlySelectedNode();
        //            e.consume();
        //          } else if (e.getClickCount() > 1) {
        //            super.mouseClicked(e);
        //            logger.debug("Ignoring dbl click event " + e);
        //          }
        //        }
        //      });

        logTree.addMouseListener(new MouseFocusOnListener());

        /**
         * We listen for when the FocusOn action changes, and then  translate
         * that to a RuleChange
         */
        addChangeListener(evt -> {
            visibilityRuleDelegate.firePropertyChange("rule", null, null);
            updateDisplay();
        });

        visibilityRuleDelegate.addPropertyChangeListener(event -> {
            if (event.getPropertyName().equals("hiddenSet")) {
                updateDisplay();
            }
        });
    }

    private void updateDisplay() {
        updateHiddenSetModels();
        updateIgnoreSummary();
        updateIgnoreExpressionSummary();
        updateAlwaysDisplayExpressionSummary();
    }

    private void updateHiddenSetModels() {
        DefaultListModel model = (DefaultListModel) ignoreList.getModel();
        model.clear();
        List sortedIgnoreList = new ArrayList(hiddenSet);
        Collections.sort(sortedIgnoreList);

        for (Object aSortedIgnoreList : sortedIgnoreList) {
            String string = (String) aSortedIgnoreList;
            model.addElement(string);
        }

//      ignoreList.setModel(model);

    }

    private void updateIgnoreSummary() {
        ignoreSummary.setText(ignoreList.getModel().getSize() + " hidden loggers");
    }

    private void updateIgnoreExpressionSummary() {
        ignoreExpressionSummary.setText(ignoreExpressionRule != null ? "Ignore (set)" : "Ignore (unset)");
    }

    private void updateAlwaysDisplayExpressionSummary() {
        alwaysDisplayExpressionSummary.setText(alwaysDisplayExpressionRule != null ? "Always displayed (set)" : "Always displayed (unset)");
    }

    private void toggleFocusOnState() {
        setFocusOnSelected(!isFocusOnSelected());
        fireChangeEvent();
    }

    public Collection getHiddenSet() {
        return Collections.unmodifiableSet(hiddenSet);
    }

    public String getHiddenExpression() {
        String text = ignoreExpressionEntryField.getText();
        if (text == null || text.trim().equals("")) {
            return null;
        }
        return text.trim();
    }

    public void setHiddenExpression(String hiddenExpression) {
        ignoreExpressionEntryField.setText(hiddenExpression);
        updateIgnoreExpression(hiddenExpression);
    }

    public String getAlwaysDisplayExpression() {
        String text = alwaysDisplayExpressionEntryField.getText();
        if (text == null || text.trim().equals("")) {
            return null;
        }
        return text.trim();
    }

    public void setAlwaysDisplayExpression(String alwaysDisplayExpression) {
        alwaysDisplayExpressionEntryField.setText(alwaysDisplayExpression);
        updateAlwaysDisplayExpression(alwaysDisplayExpression);
    }

    public void loggerNameAdded(String loggerName) {
        //no-op
    }

    public void reset() {
        expandRootLatch = false;
        //keep track if focuson was active when we were reset
        final String logger = currentlySelectedLoggerName;
        final boolean focusOnSelected = isFocusOnSelected();
        if (logger == null || !focusOnSelected) {
            return;
        }

        //loggernameAdded runs on EDT
        logTreeModel.loggerNameAdded(logger);
        EventQueue.invokeLater(() -> setFocusOn(logger));
    }

    //~ Inner Classes ===========================================================

    /**
     * DOCUMENT ME!
     *
     * @author $author$
     * @author Paul Smith &lt;psmith@apache.org&gt;
     * @version $Revision$, $Date$
     */
    private class LoggerNameTreeCellRenderer extends DefaultTreeCellRenderer {
        //~ Constructors ==========================================================

        //    private JPanel panel = new JPanel();
        private LoggerNameTreeCellRenderer() {
            super();

            //      panel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
            //      panel.add(this);
            setLeafIcon(null);
            setOpaque(false);
        }

        //~ Methods ===============================================================

        /* (non-Javadoc)
         * @see javax.swing.tree.TreeCellRenderer#getTreeCellRendererComponent(javax.swing.JTree, java.lang.Object, boolean, boolean, boolean, int, boolean)
         */

        /**
         * DOCUMENT ME!
         *
         * @param tree     DOCUMENT ME!
         * @param value    DOCUMENT ME!
         * @param sel      DOCUMENT ME!
         * @param expanded DOCUMENT ME!
         * @param leaf     DOCUMENT ME!
         * @param row      DOCUMENT ME!
         * @param focus    DOCUMENT ME!
         * @return DOCUMENT ME!
         */
        public Component getTreeCellRendererComponent(
            JTree tree, Object value, boolean sel, boolean expanded, boolean leaf,
            int row, boolean focus) {
            JLabel component =
                (JLabel) super.getTreeCellRendererComponent(
                    tree, value, sel, expanded, leaf, row, focus);

            Font originalFont = new Font(component.getFont().getName(), component.getFont().getStyle(), component.getFont().getSize());

            int style = Font.PLAIN;

            if (sel && focusOnLoggerButton.isSelected()) {
                style = style | Font.BOLD;
            }

            String logger =
                getLoggerName(
                    new TreePath(((DefaultMutableTreeNode) value).getPath()));

            if (hiddenSet.contains(logger)) {
                //        component.setEnabled(false);
                //        component.setIcon(leaf?null:getDefaultOpenIcon());
                style = style | Font.ITALIC;

                //        logger.debug("TreeRenderer: '" + logger + "' is in hiddenSet, italicizing");
            } else {
                //          logger.debug("TreeRenderer: '" + logger + "' is NOT in hiddenSet, leaving plain");
                //        component.setEnabled(true);
            }

            if (originalFont != null) {
                Font font2 = originalFont.deriveFont(style);

                if (font2 != null) {
                    component.setFont(font2);
                }
            }

            return component;
        }
    }

    private class LoggerTreePopupMenu extends JPopupMenu {
        //~ Instance fields =======================================================

        JCheckBoxMenuItem focusOnCheck = new JCheckBoxMenuItem();
        JCheckBoxMenuItem hideCheck = new JCheckBoxMenuItem();

        //~ Constructors ==========================================================

        private LoggerTreePopupMenu() {
            initMenu();
        }

        //~ Methods ===============================================================

        /* (non-Javadoc)
         * @see javax.swing.JPopupMenu#show(java.awt.Component, int, int)
         */

        /**
         * DOCUMENT ME!
         *
         * @param invoker DOCUMENT ME!
         * @param x       DOCUMENT ME!
         * @param y       DOCUMENT ME!
         */
        public void show(Component invoker, int x, int y) {
            DefaultMutableTreeNode node =
                (DefaultMutableTreeNode) logTree.getLastSelectedPathComponent();

            if (node == null) {
                return;
            }

            super.show(invoker, x, y);
        }

        /**
         * DOCUMENT ME!
         */
        private void initMenu() {
            focusOnCheck.setAction(focusOnAction);
            hideCheck.setAction(hideAction);
            add(expandAction);
            add(collapseAction);
            addSeparator();
            add(focusOnCheck);
            add(hideCheck);
            addSeparator();
            add(setRefineFocusAction);
            add(updateRefineFocusAction);
            add(clearRefineFocusAction);
            addSeparator();
            add(findAction);
            add(updateFindAction);
            add(clearFindNextAction);

            addSeparator();
            add(defineColorRuleForLoggerAction);
            addSeparator();

            add(hideSubLoggersAction);
            add(clearIgnoreListAction);
        }
    }

    private final class MouseFocusOnListener extends MouseAdapter {
        //~ Methods ===============================================================

        /* (non-Javadoc)
         * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
         */

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mouseClicked(MouseEvent e) {
            if (
                (e.getClickCount() > 1)
                    && ((e.getModifiers() & InputEvent.CTRL_MASK) > 0)
                    && ((e.getModifiers() & InputEvent.SHIFT_MASK) > 0)) {
                ignoreLoggerAtPoint(e.getPoint());
                e.consume();
                fireChangeEvent();
            } else if (
                (e.getClickCount() > 1)
                    && ((e.getModifiers() & InputEvent.CTRL_MASK) > 0)) {
                focusAnLoggerAtPoint(e.getPoint());
                e.consume();
                fireChangeEvent();
            }
        }

        /**
         * DOCUMENT ME!
         *
         * @param point
         */
        private void focusAnLoggerAtPoint(Point point) {
            String logger = getLoggerAtPoint(point);

            if (logger != null) {
                toggleFocusOnState();
            }
        }

        /**
         * DOCUMENT ME!
         *
         * @param point
         * @return
         */
        private String getLoggerAtPoint(Point point) {
            TreePath path = logTree.getPathForLocation(point.x, point.y);

            if (path != null) {
                return getLoggerName(path);
            }

            return null;
        }

        /**
         * DOCUMENT ME!
         *
         * @param point
         */
        private void ignoreLoggerAtPoint(Point point) {
            String logger = getLoggerAtPoint(point);

            if (logger != null) {
                toggleHiddenLogger(logger);
                fireChangeEvent();
            }
        }
    }

    private final class MouseKeyIconListener extends MouseMotionAdapter
        implements MouseMotionListener {
        //~ Instance fields =======================================================

        Cursor focusOnCursor =
            Toolkit.getDefaultToolkit().createCustomCursor(
                ChainsawIcons.FOCUS_ON_ICON.getImage(), new Point(10, 10), "");
        Cursor ignoreCursor =
            Toolkit.getDefaultToolkit().createCustomCursor(
                ChainsawIcons.IGNORE_ICON.getImage(), new Point(10, 10), "");

        //~ Methods ===============================================================

        /* (non-Javadoc)
         * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
         */

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mouseMoved(MouseEvent e) {
            //      logger.debug(e.toString());
            if (
                ((e.getModifiers() & InputEvent.CTRL_MASK) > 0)
                    && ((e.getModifiers() & InputEvent.SHIFT_MASK) > 0)) {
                logTree.setCursor(ignoreCursor);
            } else if ((e.getModifiers() & InputEvent.CTRL_MASK) > 0) {
                logTree.setCursor(focusOnCursor);
            } else {
                logTree.setCursor(Cursor.getDefaultCursor());
            }
        }
    }

    class VisibilityRuleDelegate extends AbstractRule {
        public boolean evaluate(LoggingEvent e, Map matches) {
            String currentlySelectedLoggerName = getCurrentlySelectedLoggerName();
            boolean hiddenLogger = e.getLoggerName() != null && isHiddenLogger(e.getLoggerName());
            boolean hiddenExpression = (ignoreExpressionRule != null && ignoreExpressionRule.evaluate(e, null));
            boolean alwaysDisplayExpression = (alwaysDisplayExpressionRule != null && alwaysDisplayExpressionRule.evaluate(e, null));
            boolean hidden = (!alwaysDisplayExpression) && (hiddenLogger || hiddenExpression);
            if (currentlySelectedLoggerName == null) {
                //if there is no selected logger, pass if not hidden
                return !hidden;
            }
            boolean result = (e.getLoggerName() != null) && !hidden;

            if (result && isFocusOnSelected()) {
                result = (e.getLoggerName() != null && (e.getLoggerName().startsWith(currentlySelectedLoggerName + ".") || e.getLoggerName().endsWith(currentlySelectedLoggerName)));
            }

            return result;
        }

        public void firePropertyChange(String propertyName, Object oldVal, Object newVal) {
            super.firePropertyChange(propertyName, oldVal, newVal);
        }
    }

}
