Редактор ячеек JTree по-разному воспринимает щелчки мыши в зависимости от ОС

Я создал структуру рендеринга/редактора ячеек дерева, которая, по общему признанию, немного хакерская, но она отлично работает в Windows и Linux. На изображении ниже показан пример установки.

введите здесь описание изображения

Цель состоит в том, что если пользователь щелкает изображение (число) 1 или 2, приложение реагирует на этот щелчок, но не выбирает строку дерева. Если пользователь щелкает текст один или два раза, приложение реагирует на этот щелчок и делает выбор строки дерева. То, как я это реализовал, опять же, немного хакерское. В основном, когда пользователь щелкает строку дерева, отображается компонент редактора (который выглядит идентично компоненту рендерера), а компонент редактора имеет прослушиватель мыши, который может определить, где пользователь щелкнул в строке.

Тот факт, что это работает в Windows/Linux, зависит от чего-то, на что я всегда считал ненадежным. По сути, если вы щелкнете строку один раз, этот единственный щелчок (а) вызовет редактор и (б) активирует прослушиватель мыши в компоненте редактора. Вот так я хочу! Однако, когда вы пытаетесь запустить приложение на Mac OSX (10.6.2, если это имеет значение), вышеупомянутое надуманное предположение больше не соответствует действительности. Каждый раз, когда вы хотите взаимодействовать с деревом, вы должны щелкнуть дважды (один раз, чтобы активировать редактор, и еще раз, чтобы активировать прослушиватель мыши).

Приведенный ниже SSCCE может воспроизвести поведение. Конечно, если у вас нет OSX, вы не сможете воспроизвести нежелательное поведение, но, возможно, вы все же можете порекомендовать более разумный способ достижения моей цели. Следите за тем, чтобы в консоли отображалось sysout сообщений, указывающих, что происходит, когда вы нажимаете на различные части дерева.

О, и SSCCE ссылается на эти два изображения:

изображение 1iamge 2

package TreeTest;

import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.net.URL;
import java.util.EventObject;
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.CellEditorListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

@SuppressWarnings("serial")
public class TreeTest extends JComponent {

    private JFrame frame;
    private DefaultTreeModel treeModel;
    private DefaultMutableTreeNode root;
    private JTree 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 {
                    TreeTest window = new TreeTest();
                    window.frame.setVisible(true);
                    window.frame.requestFocusInWindow();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public TreeTest() {
        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 JTree(treeModel);
        tree.setEditable(true);
        tree.getSelectionModel().setSelectionMode(
            TreeSelectionModel.SINGLE_TREE_SELECTION);
        tree.setRootVisible(false);
        tree.setShowsRootHandles(false);
        tree.setCellRenderer(new TreeRenderer());
        tree.setCellEditor(new TreeEditor());
        tree.putClientProperty("JTree.lineStyle", "None");
        tree.setBackground(Color.white);

        treeModel.insertNodeInto(new DefaultMutableTreeNode("two"), root, 0);
        treeModel.insertNodeInto(new DefaultMutableTreeNode("one"), root, 0);

        TreeNode[] nodes = treeModel.getPathToRoot(root);
        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 TreeComponent extends JPanel {

        public TreeComponent(JLabel numIcon, JLabel numText) {
            this.setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
            this.add(numIcon);
            this.add(numText);
        }
    }

    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();

            TreeComponent comp = new TreeComponent(numIcon, 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 TreeEditor implements TreeCellEditor {

        private TreeRenderer rend;
        private TreeComponent editorComponent;
        private JTree tree;
        private DefaultTreeModel treeModel;
        private DefaultMutableTreeNode node;
        private String str;

        public TreeEditor() {
            rend = new TreeRenderer();
        }

        @Override
        public Component getTreeCellEditorComponent(
            final JTree tree, final Object value, boolean isSelected,
            boolean expanded, boolean leaf, int row) {
            this.tree = tree;
            treeModel = (DefaultTreeModel) tree.getModel();
            node = (DefaultMutableTreeNode) value;
            Object userObject = node.getUserObject();
            this.str = (String) userObject;

            TreeNode[] nodes = treeModel.getPathToRoot(node);
            final TreePath path = new TreePath(nodes);

            editorComponent = (TreeComponent) rend.getTreeCellRendererComponent(
                tree, value, isSelected, expanded, leaf, row, false);
            editorComponent.addMouseListener(new MouseAdapter() {
                public void mousePressed(MouseEvent e) {
                    TreeEditor.this.stopCellEditing();
                    int x = e.getX();
                    if (x >= 0 && x <= 16) {
                        System.out.println(
                            "you clicked the image for row " + str);
                    } else if (x > 16) {
                        System.out.println(
                            "you clicked the text for row " + str);
                        tree.setSelectionPath(path);
                    }
                }
            });
            return editorComponent;
        }

        @Override
        public boolean isCellEditable(EventObject anEvent) {
            return true;
        }

        @Override
        public boolean shouldSelectCell(EventObject anEvent) {
            return false;
        }

        @Override
        public boolean stopCellEditing() {
            tree.cancelEditing();
            return false;
        }

        @Override
        public Object getCellEditorValue() {
            return null;
        }

        @Override
        public void cancelCellEditing() {
        }

        @Override
        public void addCellEditorListener(CellEditorListener l) {
        }

        @Override
        public void removeCellEditorListener(CellEditorListener l) {
        }
    }
}

person The111    schedule 31.03.2013    source источник
comment
не имеет отношения или нет, не копал: ваша реализация редактора недействительна! Он не должен воздействовать на дерево напрямую, он должен уведомить своих слушателей, когда будет готов. Незначительные замечания: не создавайте новые компоненты в getXXComponent, не добавляйте прослушиватели более одного раза (этот mouseListener все равно выглядит подозрительно, поскольку он взаимодействует с деревом, чего не должен)   -  person kleopatra    schedule 01.04.2013
comment
хорошо, просто прочитайте ваши комментарии к ответу @trashgod - он не предназначен для редактора :-) И я вижу проблему: область между визуализатором и редактором - скользкая земля, что-то вроде не-редактор-но-мышь -active не поддерживается и его сложно подделать. Мое личное предпочтение в таком случае - не злоупотреблять редактором, а добавить логику управления в само дерево ... проверим, что у нас есть в инкубаторе свингкс (хотя будет наполовину испеченным)   -  person kleopatra    schedule 01.04.2013
comment
@kleopatra Спасибо, я пытаюсь рассмотреть другие варианты. Почему вы говорите не создавать новые компоненты в getXXComponent?   -  person The111    schedule 01.04.2013
comment
@kleopatra Я придумал новый подход, который, вероятно, все еще немного хакерский, но вообще не использует редактор. Буду рад вашим комментариям как известному гуру Swing. :-) См. здесь, пожалуйста. Спасибо.   -  person The111    schedule 02.04.2013
comment
на самом деле, я не понимаю, что вам нужно не использовать простой редактор: все данные, представленные различными компонентами в компоненте рендеринга/редактирования, являются свойствами вашего пользовательского объекта узла - правильно реализуйте редактор и пусть делает свое дело, тогда все встанет на свои места без всяких ухищрений :-)   -  person kleopatra    schedule 02.04.2013


Ответы (3)


Краткое описание того, как реализовать сложные редакторы ячеек. Предполагается, что все интерактивные элементы редактора действительно редактируют свойство userObject узла - как в случае с полным кодом OP.

Соавторы

  • фиктивный объект данных с несколькими свойствами
  • визуализатор с несколькими дочерними элементами, каждый из которых привязан к одному свойству объекта данных
  • редактор, который использует «живой» экземпляр компонента рендеринга, то есть добавляет слушателей для соблюдения контракта редактора по мере необходимости.

Некоторый код (очевидно, не пригодный для реального использования, просто для понимания :)

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);
person kleopatra    schedule 02.04.2013
comment
@mKorbel не так ли? Не могу вспомнить и не имеет для меня особого смысла сейчас :-) - person kleopatra; 02.04.2013
comment
+1, похоже, это работает, хотя я повторяю развлечение @mKorbel, что теперь вы поддерживаете использование редактора для этого поведения. :-) Что еще более важно, я не могу понять, почему ваш код здесь работает на OSX, а мой в OP - нет. Кажется, я не могу понять, что вы делаете по-другому, что позволяет использовать один и тот же щелчок как для (а) открытия редактора, так и (б) ИСПОЛЬЗОВАНИЯ редактора. Есть идеи? - person The111; 03.04.2013
comment
@ The111 Каждая кнопка соответствует либо логическому флагу, либо цвету GPSObject и изменяет его, что называется ... редактированием :-) Ничего особенного, просто игра по правилам для редакторов: начальный триггер (как в: должен быть, в более ранних версиях были ошибки) передается в редактор после его добавления. - person kleopatra; 03.04.2013
comment
на самом деле, я не совсем слежу за забавой @mKorbel, предвзятый я бы заподозрил недоразумение - использование JPanel в рендерере/редакторе не проблема, в то время как расширение любой JSomething с новой ролью визуализатора/редактора :-) Так что если у кого-то есть ссылка на мою проповедь против прежнего, пожалуйста, дайте знать, следует удалить ‹g› - person kleopatra; 03.04.2013
comment
1. JButton (ы) с чем-либо внутри (иконка, текст, алимент, реализованные слушатели) является (являются) лучшим из, самым ...., 2. великим, как вы, 3. моя FlameWar удалена, сохранена до плохих времен :-) 4. обратите внимание, что по-прежнему невозможно искать в собственных комментариях, вопрос был дважды закрыт, затем я удалил их 5. спасибо за четкие идеи в коде после последнего редактирования - person mKorbel; 03.04.2013
comment
Извините, мое веселье было основано на том факте, что я думал, и вы, и trashgod ранее сказали мне, что эта проблема — неподходящее место для использования редактора. Но я предполагаю, что это было потому, что мой SSCCE не иллюстрировал должным образом мое фактическое использование. Так что, наверное, мой плохой. Но я все еще в замешательстве, поскольку ваш редактор возвращает JComp со слушателями, которые улавливают первый щелчок мыши, а мой редактор также возвращает JComp со слушателями, которые не улавливать первый щелчок мыши (только на OSX). Интересно, это потому, что вы использовали прослушиватели на кнопке, а не на панели. Я должен испытать больше. - person The111; 03.04.2013
comment
@ The111 нет проблем :-) Опять же: реализация вашего редактора была недействительной, что может привести к любым непредсказуемым (читай: неожиданным) результатам. - person kleopatra; 04.04.2013

Я не уверен, что понимаю требование, но приведенный ниже пример добавляет удобную привязку клавиш и работает с любым подходом одним щелчком, показанным здесь. Я решил переопределить canEditImmediately(), как предложила kleopatra, с учетом ее предостережений относительно удобства использования.

изображение дерева

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();
            }
        });
    }
}
person trashgod    schedule 01.04.2013
comment
Спасибо, как всегда, за вклад, трэшбод. Однако я не думаю, что ответ, который я ищу, находится там. Возможно, мне следует лучше объяснить свои требования, так как я понимаю ваше замешательство. Проверьте мое фактическое приложение здесь. Нажмите CTRL+N, чтобы создать новый файл, а затем поиграйте с деревом слева. В моем приложении пользователь никогда не редактирует дерево, он просто использует его для взаимодействия. Каждый значок в строке дерева делает что-то свое, и строка не должна быть выбрана, пока пользователь не щелкнет текст. - person The111; 01.04.2013
comment
Я использую редактор, что, по общему признанию, нелогично (поскольку на самом деле никакого редактирования не происходит), потому что я не могу придумать другого способа достичь своей цели! Если бы средство рендеринга дерева дало мне возможность отображать реальные компоненты, я мог бы прикрепить к ним свои прослушиватели кликов. Но это не так, поэтому я прикрепляю их к редактору, который появляется и нажимается одним и тем же щелчком, а затем исчезает, и все это прозрачно для пользователя. Но в OSX для этого требуется два щелчка каждый раз. Я полагаю, что вы на Mac, так что, возможно, вы можете предложить еще больше информации. Надеюсь, что разработка поможет! - person The111; 01.04.2013
comment
Извините, вы пытаетесь победить делегата пользовательского интерфейса с помощью ненужного редактора — обреченный подход. Почему бы не взглянуть на Outline, который упрощает добавление компонентов строки. Работает с любым TreeModel. - person trashgod; 01.04.2013
comment
Согласен, редактор никогда не был нужен. Я придумал новый подход, который, вероятно, все еще немного хакерский, но вообще не использует редактор. Буду рад вашим комментариям. См. здесь, пожалуйста. Спасибо. - person The111; 02.04.2013

Я придумал что-то, что создает желаемое поведение и вообще не использует TreeCellEditor. Вместо этого дерево недоступно для редактирования и создается с пользовательским расширением JTree, которое переопределяет processMouseEvent. Я получил эту идею здесь.

Кажется, он работает идеально, но все еще немного хакерский (он выполняет цикл вычислений, чтобы определить, где находится начало ячейки дерева, поскольку это может варьироваться в зависимости от отступа). Также я практически отключил события типа mouseClicked и mouseReleased и контролирую JTreeMod только событиями mousePressed. Не уверен, что это укусит меня позже или это плохая практика, но мне не понравилось, что мой пользовательский код запускался 3 раза подряд для всех событий. Я также пока не смог протестировать на ОС, отличной от Windows.

Вот вывод консоли после нажатия, последовательно (1) изображение один (2) текст один (3) изображение два (4) текст два. Опять же, это прекрасно реализует мое желаемое поведение.

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!

А вот и новый 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);
            }
        }
    }
}
person The111    schedule 01.04.2013
comment
+1 за sscce, хотя я думаю, что это подчеркивает неуклюжесть подхода. В Outline это будут обычные рендереры (и, возможно, редакторы) в вашем RowModel. - person trashgod; 02.04.2013
comment
@trashgod спасибо, я скоро посмотрю на Outline, но тем временем это решение поможет мне. - person The111; 02.04.2013