Gili
Gili

Reputation: 90003

Forwarding mouseMoved() events to JTree nodes?

If I place a JCheckBox outside of a JTree it plays an animation when I hover over it. When I place the same JCheckbox inside a JTree node, it no longer receives any mouseMoved() events and no animation is played. I tried forwarding these events from the JTree to the JCheckBox but nothing shows up.

I'm guessing the problem is that the same JCheckBox instance is "stamped" by a JTree (once per node). When I forward mouseMoved() event to the shared instance, it doesn't know where to repaint itself.

Any ideas?

EDIT: Here is a self-contained testcase. Please note that making the JCheckBox clickable is outside the scope of this question (I've already done so in my application using a TreeCellEditor).

import java.awt.BorderLayout;
import java.awt.Component;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;

public class HoverBug
{
    public static class TreeRenderer implements TreeCellRenderer
    {
        private final JCheckBox checkbox = new JCheckBox();

        @Override
        public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected,
            boolean expanded, boolean leaf, int row, boolean hasFocus)
        {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
            checkbox.setSelected((Boolean) node.getUserObject());
            return checkbox;
        }
    }

    public static void main(String[] args)
    {
        JCheckBox checkbox = new JCheckBox("See... this works!");

        DefaultMutableTreeNode root = new DefaultMutableTreeNode(Boolean.TRUE);
        DefaultMutableTreeNode child1 = new DefaultMutableTreeNode(Boolean.FALSE);
        DefaultMutableTreeNode child2 = new DefaultMutableTreeNode(Boolean.FALSE);
        root.add(child1);
        root.add(child2);
        DefaultTreeModel model = new DefaultTreeModel(root);

        JTree tree = new JTree(model);
        tree.setCellRenderer(new TreeRenderer());

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().setLayout(new BorderLayout());
        frame.getContentPane().add(checkbox, BorderLayout.NORTH);
        frame.getContentPane().add(tree, BorderLayout.CENTER);
        frame.setSize(800, 600);
        frame.setVisible(true);
    }

Upvotes: 2

Views: 1695

Answers (2)

Gili
Gili

Reputation: 90003

I don't think there is a solution for two reasons:

  1. JCheckBox UI classes (e.g. BasicCheckBoxUI) invoke repaint() whenever the look is modified, but you won't see these changes unless you ask the JTree to redraw the node using DefaultTreeModel.nodeChanged(TreeNode).
  2. Even if you listen for the same model events as the UI class and trigger DefaultTreeModel.nodeChanged(TreeNode) after repaint(), some L&Fs (Notably Insubstantial) check whether the JCheckBox is inside a CellRendererPane and disable animations in such a case (probably for performance reasons).

In summary, JTree design makes it impractical to animate nodes.

For what it's worth, here is my attempt at forwarding events to the underlying node (it's buggy but you get the idea):

[...]
    MouseAdapterImpl listener = new MouseAdapterImpl(tree);
    tree.addMouseListener(listener);
    tree.addMouseMotionListener(listener);
    tree.addMouseWheelListener(listener);
[...]
private class MouseAdapterImpl implements MouseListener, MouseWheelListener, MouseMotionListener
{
    private final JTree tree;
    private int lastRow = -1;

    public MouseAdapterImpl(JTree tree)
    {
        this.tree = tree;
    }

    /**
     * Returns the mouse position relative to the JTree row.
     * <p/>
     * @param e the mouse event
     */
    private void forwardEvent(MouseEvent e)
    {
        int row = tree.getRowForLocation(e.getX(), e.getY());
        Rectangle bounds;
        Point point;
        if (row == -1)
        {
            bounds = null;
            point = null;
        }
        else
        {
            bounds = tree.getRowBounds(row);
            point = new Point(e.getX() - bounds.x, e.getY() - bounds.y);
        }
        if (lastRow != row)
        {
            if (lastRow != -1)
            {
                Rectangle lastBounds = tree.getRowBounds(lastRow);
                if (lastBounds != null)
                {
                    Point lastPoint = new Point(e.getX() - lastBounds.x, e.getY() - lastBounds.y);
                    dispatchEvent(new MouseEvent(checkbox, MouseEvent.MOUSE_EXITED,
                        System.currentTimeMillis(), 0, lastPoint.x, lastPoint.y, 0, false, 0), lastRow);
                }
            }
            if (row != -1)
            {
                dispatchEvent(new MouseEvent(checkbox, MouseEvent.MOUSE_ENTERED,
                    System.currentTimeMillis(), 0, point.x, point.y, 0, false, 0), row);
            }
        }
        lastRow = row;
        if (row == -1)
            return;
        dispatchEvent(new MouseEvent(checkbox, e.getID(),
            System.currentTimeMillis(), e.getModifiers(), point.x, point.y, e.getClickCount(),
            e.isPopupTrigger(), e.getButton()), row);
    }

    private void dispatchEvent(MouseEvent e, int row)
    {
        checkbox.dispatchEvent(e);
        TreePath pathForLocation = tree.getPathForRow(row);
        if (pathForLocation == null)
            return;
        Object lastPathComponent = pathForLocation.getLastPathComponent();
        if (lastPathComponent instanceof DefaultMutableTreeNode)
        {
            DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
            model.nodeChanged((DefaultMutableTreeNode) lastPathComponent);
        }
    }

    @Override
    public void mouseEntered(MouseEvent e)
    {
        forwardEvent(e);
    }

    @Override
    public void mouseMoved(MouseEvent e)
    {
        forwardEvent(e);
    }

    @Override
    public void mouseExited(MouseEvent e)
    {
        forwardEvent(e);
    }

    @Override
    public void mouseClicked(MouseEvent e)
    {
        forwardEvent(e);
    }

    @Override
    public void mousePressed(MouseEvent e)
    {
        forwardEvent(e);
    }

    @Override
    public void mouseReleased(MouseEvent e)
    {
        forwardEvent(e);
    }

    @Override
    public void mouseWheelMoved(MouseWheelEvent e)
    {
        forwardEvent(e);
    }

    @Override
    public void mouseDragged(MouseEvent e)
    {
        forwardEvent(e);
    }
}

EDIT: Good news. They fixed this in JavaFX: http://javafx-jira.kenai.com/browse/RT-19027

Upvotes: 1

Kylar
Kylar

Reputation: 9334

Here's what I think you're looking for below, but if you're doing anything more complicated, you may want to look into creating a CellEditor instead.:

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;

public class HoverBug {
public static class TreeRenderer implements TreeCellRenderer {
    private final JCheckBox checkbox = new JCheckBox();

    @Override
    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected,
                                                  boolean expanded, boolean leaf, int row, boolean hasFocus) {
        DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
        checkbox.setSelected((Boolean) node.getUserObject());
        checkbox.setText("Row:" + row);
        return checkbox;
    }
}

public static void main(String[] args) {
    JCheckBox checkbox = new JCheckBox("See... this works!");

    DefaultMutableTreeNode root = new DefaultMutableTreeNode(Boolean.TRUE);
    DefaultMutableTreeNode child1 = new DefaultMutableTreeNode(Boolean.FALSE);
    DefaultMutableTreeNode child2 = new DefaultMutableTreeNode(Boolean.FALSE);
    root.add(child1);
    root.add(child2);
    final DefaultTreeModel model = new DefaultTreeModel(root);

    final JTree tree = new JTree(model);
    tree.setCellRenderer(new TreeRenderer());

    tree.addMouseListener(new MouseListener() {
        @Override
        public void mouseClicked(MouseEvent e) {
            TreePath pathForLocation = tree.getPathForLocation(e.getX(), e.getY());
            Object lastPathComponent = pathForLocation.getLastPathComponent();
            if(lastPathComponent instanceof DefaultMutableTreeNode){
                Boolean oldObject = (Boolean) ((DefaultMutableTreeNode)lastPathComponent).getUserObject();
                ((DefaultMutableTreeNode)lastPathComponent).setUserObject(!oldObject);
                model.nodeChanged((DefaultMutableTreeNode)lastPathComponent);
            }
        }

        @Override
        public void mousePressed(MouseEvent e) {

        }

        @Override
        public void mouseReleased(MouseEvent e) {

        }

        @Override
        public void mouseEntered(MouseEvent e) {

        }

        @Override
        public void mouseExited(MouseEvent e) {

        }
    });


    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    frame.getContentPane().setLayout(new BorderLayout());
    frame.getContentPane().add(checkbox, BorderLayout.NORTH);
    frame.getContentPane().add(tree, BorderLayout.CENTER);
    frame.setSize(800, 600);
    frame.setVisible(true);
}
}

Upvotes: 0

Related Questions