JTree cell editor receives mouse clicks differently depending on OS

前端 未结 3 1790
伪装坚强ぢ
伪装坚强ぢ 2021-01-22 02:08

I\'ve created a tree cell renderer/editor framework that is admittedly a little bit hacky, but it works perfectly on Windows and Linux. The image below illustrates a sample set

相关标签:
3条回答
  • 2021-01-22 02:33

    A quick outline of how to implement complex cellEditors. The assumption is that all interactive elements of the editor do indeed edit a property of the node's userObject - as is the case in the OP's full code.

    The collaborators

    • a mock Data object with several properties
    • a renderer with several children, each bound to one property of the data object
    • an editor which uses an "live" instance of the rendering component, that is adds listeners to comply to editor's contract as needed

    Some code (obviously not usable for real-world usage, just to get the idea :)

    public static class ViewProvider extends AbstractCellEditor 
        implements TreeCellEditor, TreeCellRenderer {
    
        private JCheckBox firstBox;
        private JButton colorButton;
        private JComponent nodePanel;
        private JButton nameButton;
    
        private Data data;
        private boolean ignore;
    
        public ViewProvider(boolean asEditor) {
            initComponents();
            if (asEditor)
              installListeners();
        }
    
        protected void initComponents() {
            nodePanel = new JPanel();
            nodePanel.setOpaque(false);
            firstBox = new JCheckBox();
            colorButton = new JButton();
            // if we need something clickable use something ... clickable :-)
            nameButton = new JButton();
            nameButton.setContentAreaFilled(false);
            nameButton.setOpaque(true);
            nameButton.setBorderPainted(false);
            nameButton.setMargin(new Insets(0, 0, 0, 0));
            nodePanel.add(firstBox);
            nodePanel.add(colorButton);
            nodePanel.add(nameButton);
        }
    
        protected void installListeners() {
            ActionListener cancel = new ActionListener() {
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    cancelCellEditing();
                }
    
            };
            nameButton.addActionListener(cancel);
            ActionListener stop = new ActionListener() {
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    stopCellEditing();
                }
    
            };
            firstBox.addActionListener(stop);
            // Note: code for using a button to trigger opening a dialog
            // is in the tutorial, should replace this
            colorButton.addActionListener(stop);
        }
    
        @Override
        public Component getTreeCellRendererComponent(JTree tree, Object value,
                boolean selected, boolean expanded, boolean leaf, int row,
                boolean hasFocus) {
            Data data = (Data) ((DefaultMutableTreeNode) value).getUserObject();
            firstBox.setSelected(data.isVisible);
            colorButton.setBackground(data.color);
            nameButton.setText(data.name);
            nameButton.setBackground(selected ? Color.YELLOW : tree.getBackground());
            nameButton.setFont(tree.getFont());
            return nodePanel;
        }
    
        @Override
        public Component getTreeCellEditorComponent(JTree tree, Object value,
                boolean isSelected, boolean expanded, boolean leaf, int row) {
            // copy to not fiddle with the original
            data = new Data((Data) ((DefaultMutableTreeNode) value).getUserObject());
            ignore = true;
            getTreeCellRendererComponent(tree, value, isSelected, expanded, leaf, row, false);
            ignore = false;
            return nodePanel;
        }
    
        @Override
        public Object getCellEditorValue() {
            return data;
        }
    
        @Override
        public boolean shouldSelectCell(EventObject anEvent) {
            // at this point the editing component is added to the tree
            // and the mouse coordinates still in tree coordinates
            if (anEvent instanceof MouseEvent) {
                MouseEvent me = (MouseEvent) anEvent;
                Point loc = SwingUtilities.convertPoint(me.getComponent(), 
                        me.getPoint(), nodePanel);
                return loc.x >= nameButton.getX();
            }
            return false;
        }
    
        @Override
        public boolean stopCellEditing() {
            if (ignore) return false;
            // real-world data will have setters
            data.isVisible = firstBox.isSelected();
            return super.stopCellEditing();
        }
    
        @Override
        public void cancelCellEditing() {
            if (ignore) return;
            data = null;
            super.cancelCellEditing();
        }
    
    }
    
    // simple Data - obviously not for production 
    public static class Data {
        boolean isVisible;
        Color color;
        String name;
    
        public Data(boolean isVisible, Color color,
                String name) {
            this.isVisible = isVisible;
            this.color = color;
            this.name = name;
        }
    
        /**
         * A copy constructor to allow editors to manipulate its
         * properties without changing the original.
         * 
         * @param original
         */
        public Data(Data original) {
            this.isVisible = original.isVisible;
            this.color = original.color;
            this.name = original.name;
        }
    }
    
    // usage:
    DefaultMutableTreeNode root = new DefaultMutableTreeNode(
        new Data(true, Color.RED, "someName"));
    root.add(new DefaultMutableTreeNode(new Data(true, Color.GREEN, "other")));
    root.add(new DefaultMutableTreeNode(new Data(false, Color.BLUE, "whatagain")));
    root.add(new DefaultMutableTreeNode(new Data(false, Color.YELLOW, "dummy")));
    
    JTree tree = new JTree(root);
    tree.setCellRenderer(new ViewProvider(false));
    tree.setCellEditor(new ViewProvider(true));
    tree.setEditable(true);
    
    0 讨论(0)
  • 2021-01-22 02:49

    I've come up with something that creates my desired behavior and does not use a TreeCellEditor at all. Instead, the tree is uneditable, and is instantiated with a custom extension of JTree which has processMouseEvent overridden. I got this idea here.

    It seems to work perfect, but it is still a little bit hacky (it does a loop of calculations to determine where the start of the tree cell is, since that can vary based on indentation). Also I pretty much disabled mouseClicked and mouseReleased type events, and am controlling the JTreeMod only with mousePressed events. Not sure if this will bite me later or is bad practice, but I didn't like my custom code running 3 times in a row for all the events. I also haven't been able to test on a non-Windows OS just yet.

    Here is the console output after clicking, in series (1) the image one (2) the text one (3) the image two (4) the text two. Again, this perfectly implements my desired behavior.

    you clicked the image for row 1. this was detected, but no selection will happen!
    you clicked the text for row 1. this was detected, and selection WILL happen!
    SELECTION CHANGED!
    you clicked the image for row 2. this was detected, but no selection will happen!
    you clicked the text for row 2. this was detected, and selection WILL happen!
    SELECTION CHANGED!
    

    And here is the new SSCCE:

    package TreeTest;
    
    import java.awt.Color;
    import java.awt.Component;
    import java.awt.EventQueue;
    import java.awt.event.MouseEvent;
    import java.net.URL;
    
    import javax.imageio.ImageIO;
    import javax.swing.BoxLayout;
    import javax.swing.ImageIcon;
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.JTree;
    import javax.swing.UIManager;
    import javax.swing.border.EmptyBorder;
    import javax.swing.border.LineBorder;
    import javax.swing.event.TreeSelectionEvent;
    import javax.swing.event.TreeSelectionListener;
    import javax.swing.tree.DefaultMutableTreeNode;
    import javax.swing.tree.DefaultTreeModel;
    import javax.swing.tree.TreeCellRenderer;
    import javax.swing.tree.TreeNode;
    import javax.swing.tree.TreePath;
    import javax.swing.tree.TreeSelectionModel;
    
    @SuppressWarnings("serial")
    public class TreeTest2 extends JComponent {
    
        private JFrame frame;
        private DefaultTreeModel treeModel;
        private DefaultMutableTreeNode root;
        private JTreeMod tree;
    
        public static void main(String[] args) {
            try {
                UIManager.setLookAndFeel(
                    UIManager.getSystemLookAndFeelClassName());
            } catch (Throwable e) {
                e.printStackTrace();
            }
            EventQueue.invokeLater(new Runnable() {
                public void run() {
                    try {
                        TreeTest2 window = new TreeTest2();
                        window.frame.setVisible(true);
                        window.frame.requestFocusInWindow();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    
        public TreeTest2() {
            initialize();
        }
    
        private void initialize() {
            frame = new JFrame("Tree Test");
            frame.setBounds(400, 400, 250, 150);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            root = new DefaultMutableTreeNode("root");
            treeModel = new DefaultTreeModel(root);
            tree = new JTreeMod(treeModel);
            tree.setEditable(false);
            tree.getSelectionModel().setSelectionMode(
                TreeSelectionModel.SINGLE_TREE_SELECTION);
            tree.setRootVisible(false);
            tree.setShowsRootHandles(true);
            tree.setCellRenderer(new TreeRenderer());
            tree.putClientProperty("JTree.lineStyle", "None");
            tree.setBackground(Color.white);
    
            DefaultMutableTreeNode one = new DefaultMutableTreeNode("one");
            DefaultMutableTreeNode two = new DefaultMutableTreeNode("two");
    
            treeModel.insertNodeInto(one, root, 0);
            treeModel.insertNodeInto(two, one, 0);
    
            TreeNode[] nodes = treeModel.getPathToRoot(root);
            tree.expandPath(new TreePath(nodes));
            nodes = treeModel.getPathToRoot(one);
            tree.expandPath(new TreePath(nodes));
            tree.addTreeSelectionListener(new TreeSelectionListener() {
                @Override
                public void valueChanged(TreeSelectionEvent e) {
                    System.out.println("SELECTION CHANGED!");
                }
            });
    
            frame.getContentPane().add(tree);
        }
    
        public class TreeRenderer implements TreeCellRenderer {
    
            private ImageIcon oneIcon;
            private ImageIcon twoIcon;
    
            public TreeRenderer() {
                try {
                    oneIcon = new ImageIcon(ImageIO.read(
                            new URL("http://i.imgur.com/HtHJkfI.png")));
                    twoIcon = new ImageIcon(ImageIO.read(
                            new URL("http://i.imgur.com/w5jAp5c.png")));
                } catch (Exception e) {
                    e.printStackTrace();
                }
    
            }
    
            @Override
            public Component getTreeCellRendererComponent(JTree tree, Object value,
                boolean selected, boolean expanded, boolean leaf, int row,
                boolean hasFocus) {
                JLabel numIcon = new JLabel();
                numIcon.setAlignmentX(JLabel.CENTER_ALIGNMENT);
                numIcon.setAlignmentY(JLabel.CENTER_ALIGNMENT);
                numIcon.setBorder(new EmptyBorder(0, 0, 0, 4));
    
                JLabel numText = new JLabel();
    
                JPanel comp = new JPanel();
                comp.setLayout(new BoxLayout(comp, BoxLayout.X_AXIS));
                comp.add(numIcon);
                comp.add(numText);
    
                String str = (String) ((DefaultMutableTreeNode) value).getUserObject();
                if (str.equals("one")) {
                    numIcon.setIcon(oneIcon);
                    numText.setText("one");
                } else if (str.equals("two")) {
                    numIcon.setIcon(twoIcon);
                    numText.setText("two");
                }
    
                numText.setOpaque(true);
                if (selected) {
                    numText.setBackground(new Color(209, 230, 255));
                    numText.setBorder(new LineBorder(
                        new Color(132, 172, 221), 1, false));
                } else {
                    numText.setBackground(Color.white);
                    numText.setBorder(new LineBorder(Color.white, 1, false));
                }
                comp.setFocusable(false);
                comp.setBackground(Color.white);
                return comp;
            }
        }
    
        public class JTreeMod extends JTree {
            public JTreeMod(DefaultTreeModel treeModel) {
                super(treeModel);
            }
    
            @Override
            protected void processMouseEvent(MouseEvent e) {
                int type = e.getID();
                if (type == MouseEvent.MOUSE_CLICKED || type == MouseEvent.MOUSE_RELEASED) {
                    // do nothing
                } else if (type == MouseEvent.MOUSE_PRESSED) {
                    int x = e.getX();
                    int y = e.getY();
    
                    int row = this.getRowForLocation(x, y);
                    if (row == -1) {
                        super.processMouseEvent(e);
                        return;
                    }
    
                    int xOffset = x;
                    int row1 = row;
                    while (row1 == row) {
                        xOffset--;
                        row1 = this.getRowForLocation(xOffset, y);
                    }
                    xOffset++;
    
                    if (x - xOffset <= 16) {
                        System.out.println("you clicked the image for row " + (row + 1) +
                                ". this was detected, but no selection will happen!");
                        return;
                    } else {
                        System.out.println("you clicked the text for row " + (row + 1) + 
                                ". this was detected, and selection WILL happen!");
                        super.processMouseEvent(e);
                    }
                } else {
                    super.processMouseEvent(e);
                }
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-22 02:52

    I'm not sure I understand the requirement, but the example below adds a convenient key binding and works with either single-click approach shown here. I've chosen to override canEditImmediately(), as suggested by kleopatra, subject to her caveats concerning usability.

    tree image

    import java.awt.Component;
    import java.awt.EventQueue;
    import java.awt.event.KeyEvent;
    import java.awt.event.MouseEvent;
    import java.net.URL;
    import java.util.EventObject;
    import javax.imageio.ImageIO;
    import javax.swing.Icon;
    import javax.swing.ImageIcon;
    import javax.swing.JFrame;
    import javax.swing.JScrollPane;
    import javax.swing.JTree;
    import javax.swing.KeyStroke;
    import javax.swing.SwingUtilities;
    import javax.swing.tree.DefaultMutableTreeNode;
    import javax.swing.tree.DefaultTreeCellEditor;
    import javax.swing.tree.DefaultTreeCellRenderer;
    
    /**
     * @see https://stackoverflow.com/a/15738813/230513
     * @see https://stackoverflow.com/q/15625424/230513
     */
    public class Test {
    
        private static Icon one;
        private static Icon two;
    
        private void display() {
            JFrame f = new JFrame("Test");
            f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            final JTree tree = new JTree();
            for (int i = 0; i < tree.getRowCount(); i++) {
                tree.expandRow(i);
            }
            final TreeRenderer renderer = new TreeRenderer();
            tree.setCellRenderer(renderer);
            tree.setCellEditor(new TreeEditor(tree, renderer));
            tree.setEditable(true);
            tree.getInputMap().put(
                KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "startEditing");
            f.add(new JScrollPane(tree));
            f.pack();
            f.setLocationRelativeTo(null);
            f.setVisible(true);
        }
    
        private static class TreeRenderer extends DefaultTreeCellRenderer {
    
            @Override
            public Component getTreeCellRendererComponent(JTree tree, Object value,
                boolean sel, boolean exp, boolean leaf, int row, boolean hasFocus) {
                DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
                String s = node.getUserObject().toString();
                if ("colors".equals(s)) {
                    setOpenIcon(one);
                    setClosedIcon(one);
                } else if ("sports".equals(s)) {
                    setOpenIcon(two);
                    setClosedIcon(two);
                } else {
                    setOpenIcon(getDefaultOpenIcon());
                    setClosedIcon(getDefaultClosedIcon());
                }
                super.getTreeCellRendererComponent(
                    tree, value, sel, exp, leaf, row, hasFocus);
                return this;
            }
        }
    
        private static class TreeEditor extends DefaultTreeCellEditor {
    
            public TreeEditor(JTree tree, DefaultTreeCellRenderer renderer) {
                super(tree, renderer);
            }
    
            @Override
            public Component getTreeCellEditorComponent(JTree tree, Object value,
                boolean isSelected, boolean exp, boolean leaf, int row) {
                Component c = super.getTreeCellEditorComponent(
                    tree, value, isSelected, exp, leaf, row);
                DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
                String s = node.getUserObject().toString();
                if ("colors".equals(s)) {
                    editingIcon = one;
                } else if ("sports".equals(s)) {
                    editingIcon = two;
                }
                return c;
            }
    
            @Override
            protected boolean canEditImmediately(EventObject event) {
                if ((event instanceof MouseEvent)
                    && SwingUtilities.isLeftMouseButton((MouseEvent) event)) {
                    MouseEvent me = (MouseEvent) event;
    
                    return ((me.getClickCount() >= 1)
                        && inHitRegion(me.getX(), me.getY()));
                }
                return (event == null);
            }
        }
    
        public static void main(String[] args) throws Exception {
            one = new ImageIcon(ImageIO.read(
                new URL("http://i.imgur.com/HtHJkfI.png")));
            two = new ImageIcon(ImageIO.read(
                new URL("http://i.imgur.com/w5jAp5c.png")));
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    new Test().display();
                }
            });
        }
    }
    
    0 讨论(0)
提交回复
热议问题