Search code examples
javaswingjlistrenderer

How to make a JList that each Item contains JCheckBox and JLabel with different events on click


I'm trying to build a Jlist that contains elements (custom) with separated listeners and behaviors. I mean, by this, that when the Cell its loaded it should:

  • Display a Checkbox that is checked if the CustomClass has the value "installed" = true
  • Display the Text that is in the CustomClass
  • If I click in the CheckBox it will change the state of Installed of that Object to the opposite of its value
  • If I click the text, it will display the info of the CustomClass in another panel

Right now what I managed to build it's the JList, with the CustomClass (SimpleTemplate), that draw a Checkbox with the SimpleTemplate's Name and, when you click on it, displays the info of the SimpleTemplate in another panel. Yet, I dont know how to separate the listeners and the events and proposed before.

My code so far is as follows:

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import javax.swing.DefaultListModel;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;

public class CustomJListExample extends JFrame {

    private static final Dimension SIDE_PANEL_DIMENSION = new Dimension(190, 190);
    private static final Dimension CONTAINER_PANEL_DIMENSION = new Dimension(400, 200);
    private static final Dimension TEMPLATES_LIST_DIMENSION = new Dimension(180, 180);
    private static final Border SIMPLE_BORDER = new JTextField().getBorder();

    private JList<SimpleTemplate> templatesList = new JList<>();
    private JLabel templateName = new JLabel();
    private JLabel templateDescription = new JLabel();


    public CustomJListExample() {
        JPanel rightPanel = prepareRightSide();
        JPanel leftPanel = prepareLeftSide();

        JPanel containerPanel = new JPanel();
        containerPanel.setPreferredSize(CONTAINER_PANEL_DIMENSION);

        containerPanel.add(leftPanel);
        containerPanel.add(rightPanel);
        add(containerPanel);
        pack();
    }

    private JPanel prepareRightSide() {
        JPanel rightPanel = new JPanel();
        rightPanel.setBorder(SIMPLE_BORDER);
        rightPanel.setBackground(Color.GRAY);
        rightPanel.setPreferredSize(SIDE_PANEL_DIMENSION);

        templateName.setText("---");
        templateDescription.setText("---");

        rightPanel.add(templateName);
        rightPanel.add(templateDescription);

        return rightPanel;
    }

    private JPanel prepareLeftSide() {
        JPanel leftPanel = new JPanel();
        leftPanel.setBorder(SIMPLE_BORDER);
        leftPanel.setBackground(Color.GRAY);
        leftPanel.setPreferredSize(SIDE_PANEL_DIMENSION);

        DefaultListModel<SimpleTemplate> templatesListModel = new DefaultListModel<>();
        templatesListModel.addElement(new SimpleTemplate("Template 1", "Description template 1", false));
        templatesListModel.addElement(new SimpleTemplate("Template 2", "Description template 2", true));
        templatesListModel.addElement(new SimpleTemplate("Template 3", "Description template 3", false));

        templatesList.setCellRenderer(new JListRepositoryItem());
        templatesList.addListSelectionListener(e-> displayTemplateInfo());
        templatesList.setPreferredSize(TEMPLATES_LIST_DIMENSION);
        templatesList.setModel(templatesListModel);
        templatesList.repaint();

        leftPanel.add(templatesList);

        return leftPanel;
    }

    private void displayTemplateInfo() {
        SimpleTemplate selectedValue = templatesList.getSelectedValue();
        templateName.setText(selectedValue.getName());
        templateDescription.setText(selectedValue.getDescription());
    }

    class JListRepositoryItem extends JCheckBox implements ListCellRenderer {
        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index,
            boolean isSelected, boolean cellHasFocus) {
            setComponentOrientation(list.getComponentOrientation());
            setFont(list.getFont());
            setBackground(list.getBackground());
            setForeground(list.getForeground());

            if (value instanceof SimpleTemplate) {
                SimpleTemplate template = (SimpleTemplate) value;
                setSelected(isSelected);
                setEnabled(list.isEnabled());
                setText(template.getName());
            }

            return this;
        }
    }

    class SimpleTemplate {
        private String name;
        private String description;
        private boolean installed;

        public SimpleTemplate(String name, String description, boolean installed) {
            this.name = name;
            this.description = description;
            this.installed = installed;
        }

        public String getName() {
            return name;
        }

        public String getDescription() {
            return description;
        }

        public boolean isInstalled() {
            return installed;
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new CustomJListExample().setVisible(true));
    }
}

That generates the following Example.

enter image description here

Yet, I don't manage to make the Text have its own behavior, and CheckBox its own behavior as well.


Solution

  • If you want to do it properly - you will have to change JList UI implementation since the selection behavior is coming from there. That is a quite hard thing to do if you haven't ever worked with it.

    Also, generally it is hard to do something like what you asked because JList component does not allow you to interact with the components provided in ListCellRenderer implementation directly - it simply uses them to "stamp" their graphical representation multiple times with different settings. That makes JList perform extremely well on large amounts of data, but locks you out of direct interaction with the renderer components.

    But there is a workaround you could use for simple cases like yours - you can add a custom MouseListener to your list and "guess" where the user clicks. Luckily JList API provides you will all the methods necessary to do that:

    templatesList.addMouseListener ( new MouseAdapter ()
    {
        @Override
        public void mousePressed ( final MouseEvent e )
        {
            final Point point = e.getPoint ();
            final int index = templatesList.locationToIndex ( point );
            if ( index != -1 )
            {
                // Next calculations assume that text is aligned to left, but are easy to adjust
                final SimpleTemplate element = templatesList.getModel ().getElementAt ( index );
                final Rectangle cellBounds = templatesList.getCellBounds ( index, index );
                final JListRepositoryItem renderer = ( JListRepositoryItem ) templatesList.getCellRenderer ();
                final int iconWidth = renderer.getIcon () !=null ? renderer.getIcon ().getIconWidth () : 16;
                final Insets insets = renderer.getInsets ();
                final int iconX = cellBounds.x + insets.left;
    
                // Ensure that mouse press happened within top/bottom insets
                if ( cellBounds.y + insets.top <= point.y && point.y <= cellBounds.y + cellBounds.height - insets.bottom )
                {
                    // Check whether we hit the checkbox icon
                    if ( iconX <= point.x && point.x <= cellBounds.x + insets.left + iconWidth )
                    {
                        // We hit the checkbox icon
                        element.installed = !element.installed;
                        templatesList.repaint ( cellBounds );
                    }
                    else
                    {
                        // Check whether we hit text
                        final int iconTextGap = renderer.getIconTextGap ();
                        final int textX = cellBounds.x + insets.left + iconWidth + iconTextGap;
                        final FontMetrics fontMetrics = renderer.getFontMetrics ( renderer.getFont () );
                        final int textWidth = fontMetrics.stringWidth ( element.getName () );
                        if ( textX <= point.x && point.x <= textX + textWidth )
                        {
                            // We hit the text
                            templateName.setText ( element.getName () );
                            templateDescription.setText ( element.getDescription () );
                        }
                        else
                        {
                            // Reset values
                            templateName.setText ( "---" );
                            templateDescription.setText ( "---" );
                        }
                    }
                }
                else
                {
                    // Reset values
                    templateName.setText ( "---" );
                    templateDescription.setText ( "---" );
                }
            }
            else
            {
                // Reset values
                templateName.setText ( "---" );
                templateDescription.setText ( "---" );
            }
        }
    } );
    
    

    I've added text size calculation for demonstration purpose, but you can simplify the whole thing if you don't need that.

    Here is a breakdown of any basic component like label, button or checkbox:

    Component contents breakdown

    This should make it easier to visualize and should help you understand which area you want to make "clickable" as this isn't really always a simple thing to decide. For instance my example is pretty precise - you can click only exactly on the check icon or the text, but on practice that will be terrible experience and you probably would want to expand it up to insets/leftover areas.

    You'll also need to remove your ListSelectionListener as it will be conflicting with the MouseListener. Here is the full code:

    import javax.swing.*;
    import javax.swing.border.Border;
    import java.awt.*;
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    
    public class CustomJListExample extends JFrame
    {
    
        private static final Dimension SIDE_PANEL_DIMENSION = new Dimension ( 190, 190 );
        private static final Dimension CONTAINER_PANEL_DIMENSION = new Dimension ( 400, 200 );
        private static final Dimension TEMPLATES_LIST_DIMENSION = new Dimension ( 180, 180 );
        private static final Border SIMPLE_BORDER = new JTextField ().getBorder ();
    
        private JList<SimpleTemplate> templatesList = new JList<> ();
        private JLabel templateName = new JLabel ();
        private JLabel templateDescription = new JLabel ();
    
    
        public CustomJListExample ()
        {
            JPanel rightPanel = prepareRightSide ();
            JPanel leftPanel = prepareLeftSide ();
    
            JPanel containerPanel = new JPanel ();
            containerPanel.setPreferredSize ( CONTAINER_PANEL_DIMENSION );
    
            containerPanel.add ( leftPanel );
            containerPanel.add ( rightPanel );
            add ( containerPanel );
            pack ();
        }
    
        private JPanel prepareRightSide ()
        {
            JPanel rightPanel = new JPanel ();
            rightPanel.setBorder ( SIMPLE_BORDER );
            rightPanel.setBackground ( Color.GRAY );
            rightPanel.setPreferredSize ( SIDE_PANEL_DIMENSION );
    
            templateName.setText ( "---" );
            templateDescription.setText ( "---" );
    
            rightPanel.add ( templateName );
            rightPanel.add ( templateDescription );
    
            return rightPanel;
        }
    
        private JPanel prepareLeftSide ()
        {
            JPanel leftPanel = new JPanel ();
            leftPanel.setBorder ( SIMPLE_BORDER );
            leftPanel.setBackground ( Color.GRAY );
            leftPanel.setPreferredSize ( SIDE_PANEL_DIMENSION );
    
            DefaultListModel<SimpleTemplate> templatesListModel = new DefaultListModel<> ();
            templatesListModel.addElement ( new SimpleTemplate ( "Template 1", "Description template 1", false ) );
            templatesListModel.addElement ( new SimpleTemplate ( "Template 2", "Description template 2", true ) );
            templatesListModel.addElement ( new SimpleTemplate ( "Template 3", "Description template 3", false ) );
    
            templatesList.setCellRenderer ( new JListRepositoryItem () );
            templatesList.setPreferredSize ( TEMPLATES_LIST_DIMENSION );
            templatesList.setModel ( templatesListModel );
            templatesList.repaint ();
    
            templatesList.addMouseListener ( new MouseAdapter ()
            {
                @Override
                public void mousePressed ( final MouseEvent e )
                {
                    final Point point = e.getPoint ();
                    final int index = templatesList.locationToIndex ( point );
                    if ( index != -1 )
                    {
                        // Next calculations assume that text is aligned to left, but are easy to adjust
                        final SimpleTemplate element = templatesList.getModel ().getElementAt ( index );
                        final Rectangle cellBounds = templatesList.getCellBounds ( index, index );
                        final JListRepositoryItem renderer = ( JListRepositoryItem ) templatesList.getCellRenderer ();
                        final int iconWidth = renderer.getIcon () !=null ? renderer.getIcon ().getIconWidth () : 16;
                        final Insets insets = renderer.getInsets ();
                        final int iconX = cellBounds.x + insets.left;
    
                        // Ensure that mouse press happened within top/bottom insets
                        if ( cellBounds.y + insets.top <= point.y && point.y <= cellBounds.y + cellBounds.height - insets.bottom )
                        {
                            // Check whether we hit the checkbox icon
                            if ( iconX <= point.x && point.x <= cellBounds.x + insets.left + iconWidth )
                            {
                                // We hit the checkbox icon
                                element.installed = !element.installed;
                                templatesList.repaint ( cellBounds );
                            }
                            else
                            {
                                // Check whether we hit text
                                final int iconTextGap = renderer.getIconTextGap ();
                                final int textX = cellBounds.x + insets.left + iconWidth + iconTextGap;
                                final FontMetrics fontMetrics = renderer.getFontMetrics ( renderer.getFont () );
                                final int textWidth = fontMetrics.stringWidth ( element.getName () );
                                if ( textX <= point.x && point.x <= textX + textWidth )
                                {
                                    // We hit the text
                                    templateName.setText ( element.getName () );
                                    templateDescription.setText ( element.getDescription () );
                                }
                                else
                                {
                                    // Reset values
                                    templateName.setText ( "---" );
                                    templateDescription.setText ( "---" );
                                }
                            }
                        }
                        else
                        {
                            // Reset values
                            templateName.setText ( "---" );
                            templateDescription.setText ( "---" );
                        }
                    }
                    else
                    {
                        // Reset values
                        templateName.setText ( "---" );
                        templateDescription.setText ( "---" );
                    }
                }
            } );
    
            leftPanel.add ( templatesList );
    
            return leftPanel;
        }
    
        class JListRepositoryItem extends JCheckBox implements ListCellRenderer<SimpleTemplate>
        {
            @Override
            public Component getListCellRendererComponent ( JList list, SimpleTemplate value, int index,
                                                            boolean isSelected, boolean cellHasFocus )
            {
                setComponentOrientation ( list.getComponentOrientation () );
                setFont ( list.getFont () );
                setBackground ( list.getBackground () );
                setForeground ( list.getForeground () );
    
                setSelected ( value.isInstalled () );
                setEnabled ( list.isEnabled () );
                setText ( value.getName () );
    
                return this;
            }
        }
    
        class SimpleTemplate
        {
            private String name;
            private String description;
            private boolean installed;
    
            public SimpleTemplate ( String name, String description, boolean installed )
            {
                this.name = name;
                this.description = description;
                this.installed = installed;
            }
    
            public String getName ()
            {
                return name;
            }
    
            public String getDescription ()
            {
                return description;
            }
    
            public boolean isInstalled ()
            {
                return installed;
            }
        }
    
        public static void main ( String[] args )
        {
            SwingUtilities.invokeLater ( () -> new CustomJListExample ().setVisible ( true ) );
        }
    }
    

    Although I do want to emphasize again that this is a "hack" that works outside of the JList internal logic, so you cannot rely on the JList selection as it will be changed by the internal lsiteners from the JList UI. But it doesn't seem that you really need the JList selection in the first place, so this might work just fine for you.

    In case you would be willing to adjust JList UI - you will need to perform similar calculations, but also provide a custom ListUI implementation which may be hard if you are using native OS Look and Feel.