2

I have been googling hard trying to find a JList implementation with categories. I guess I could implement one myself, but the cell renderer, model and everything is a bit of a pain. Therefore I turn to you!

My question is this: If I have a list of items, assigned to categories, I could display them in a JTree. But since I know the depth is never more than 2, I feel the JTree is overkill. Do you know if there is an easy way in Swing (or in some external library) to make a JList like this:

Jlist with categories http://dl.dropbox.com/u/4607638/jlist-cats.png

Where the blue fields are labels (not selectable), and the white fields are normal list cells?

Thanks in advance for any tips and help!

andli
  • 301
  • 5
  • 14
  • 1
    The concept you are looking for is called an 'OutlookBar' if I am not mistaken. Perhaps that googling on this term results in some code you can freely use (I did a quick search but found mainly commercial solutions) – Robin Apr 24 '12 at 13:56

4 Answers4

6

is possible

enter image description hereenter image description hereenter image description here

import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.image.BufferedImage;
import javax.swing.*;

public class ExpandingPanels extends MouseAdapter {

    private ActionPanel[] aps;
    private JPanel[] panels;

    public ExpandingPanels() {
        assembleActionPanels();
        assemblePanels();
    }

    @Override
    public void mousePressed(MouseEvent e) {
        ActionPanel ap = (ActionPanel) e.getSource();
        if (ap.target.contains(e.getPoint())) {
            ap.toggleSelection();
            togglePanelVisibility(ap);
        }
    }

    private void togglePanelVisibility(ActionPanel ap) {
        int index = getPanelIndex(ap);
        if (panels[index].isShowing()) {
            panels[index].setVisible(false);
        } else {
            panels[index].setVisible(true);
        }
        ap.getParent().validate();
    }

    private int getPanelIndex(ActionPanel ap) {
        for (int j = 0; j < aps.length; j++) {
            if (ap == aps[j]) {
                return j;
            }
        }
        return -1;
    }

    private void assembleActionPanels() {
        String[] ids = {"level 1", "level 2", "level 3", "level 4"};
        aps = new ActionPanel[ids.length];
        for (int j = 0; j < aps.length; j++) {
            aps[j] = new ActionPanel(ids[j], this);
        }
    }

    private void assemblePanels() {
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(2, 1, 2, 1);
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;
        JPanel p1 = new JPanel(new GridBagLayout());
        gbc.gridwidth = GridBagConstraints.RELATIVE;
        p1.add(new JButton("button 1"), gbc);
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        p1.add(new JButton("button 2"), gbc);
        gbc.gridwidth = GridBagConstraints.RELATIVE;
        p1.add(new JButton("button 3"), gbc);
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        p1.add(new JButton("button 4"), gbc);
        JPanel p2 = new JPanel(new GridBagLayout());
        gbc.gridwidth = 1;
        gbc.anchor = GridBagConstraints.EAST;
        p2.add(new JLabel("enter"), gbc);
        gbc.anchor = GridBagConstraints.WEST;
        p2.add(new JTextField(8), gbc);
        gbc.anchor = GridBagConstraints.CENTER;
        p2.add(new JButton("button 1"), gbc);
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        p2.add(new JButton("button 2"), gbc);
        JPanel p3 = new JPanel(new BorderLayout());
        JTextArea textArea = new JTextArea(8, 12);
        textArea.setLineWrap(true);
        p3.add(new JScrollPane(textArea));
        JPanel p4 = new JPanel(new GridBagLayout());
        addComponents(new JLabel("label 1"), new JTextField(12), p4, gbc);
        addComponents(new JLabel("label 2"), new JTextField(16), p4, gbc);
        gbc.gridwidth = 2;
        gbc.gridy = 2;
        p4.add(new JSlider(), gbc);
        gbc.gridy++;
        JPanel p5 = new JPanel(new GridBagLayout());
        p5.add(new JButton("button 1"), gbc);
        p5.add(new JButton("button 2"), gbc);
        p5.add(new JButton("button 3"), gbc);
        p5.add(new JButton("button 4"), gbc);
        gbc.weighty = 1.0;
        gbc.fill = GridBagConstraints.BOTH;
        p4.add(p5, gbc);
        panels = new JPanel[]{p1, p2, p3, p4};
    }

    private void addComponents(Component c1, Component c2, Container c, GridBagConstraints gbc) {
        gbc.anchor = GridBagConstraints.EAST;
        gbc.gridwidth = GridBagConstraints.RELATIVE;
        c.add(c1, gbc);
        gbc.anchor = GridBagConstraints.WEST;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        c.add(c2, gbc);
        gbc.anchor = GridBagConstraints.CENTER;
    }

    private JPanel getComponent() {
        JPanel panel = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(1, 3, 0, 3);
        gbc.weightx = 1.0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        for (int j = 0; j < aps.length; j++) {
            panel.add(aps[j], gbc);
            panel.add(panels[j], gbc);
            panels[j].setVisible(false);
        }
        JLabel padding = new JLabel();
        gbc.weighty = 1.0;
        panel.add(padding, gbc);
        return panel;
    }

    public static void main(String[] args) {
        ExpandingPanels test = new ExpandingPanels();
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().add(new JScrollPane(test.getComponent()));
        f.setSize(360, 500);
        f.setLocation(200, 100);
        f.setVisible(true);
    }
}

class ActionPanel extends JPanel {

    private static final long serialVersionUID = 1L;
    private String text;
    private Font font;
    private boolean selected;
    private BufferedImage open, closed;
    public Rectangle target;
    private final int OFFSET = 30, PAD = 5;

    public ActionPanel(String text, MouseListener ml) {
        this.text = text;
        addMouseListener(ml);
        font = new Font("sans-serif", Font.PLAIN, 12);
        selected = false;
        setBackground(new Color(200, 200, 220));
        setPreferredSize(new Dimension(200, 20));
        setBorder(BorderFactory.createRaisedBevelBorder());
        setPreferredSize(new Dimension(200, 20));
        createImages();
        setRequestFocusEnabled(true);
    }

    public void toggleSelection() {
        selected = !selected;
        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        int w = getWidth();
        int h = getHeight();
        if (selected) {
            g2.drawImage(open, PAD, 0, this);
        } else {
            g2.drawImage(closed, PAD, 0, this);
        }
        g2.setFont(font);
        FontRenderContext frc = g2.getFontRenderContext();
        LineMetrics lm = font.getLineMetrics(text, frc);
        float height = lm.getAscent() + lm.getDescent();
        float x = OFFSET;
        float y = (h + height) / 2 - lm.getDescent();
        g2.drawString(text, x, y);
    }

    private void createImages() {
        int w = 20;
        int h = getPreferredSize().height;
        target = new Rectangle(2, 0, 20, 18);
        open = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = open.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setPaint(getBackground());
        g2.fillRect(0, 0, w, h);
        int[] x = {2, w / 2, 18};
        int[] y = {4, 15, 4};
        Polygon p = new Polygon(x, y, 3);
        g2.setPaint(Color.green.brighter());
        g2.fill(p);
        g2.setPaint(Color.blue.brighter());
        g2.draw(p);
        g2.dispose();
        closed = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        g2 = closed.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setPaint(getBackground());
        g2.fillRect(0, 0, w, h);
        x = new int[]{3, 13, 3};
        y = new int[]{4, h / 2, 16};
        p = new Polygon(x, y, 3);
        g2.setPaint(Color.red);
        g2.fill(p);
        g2.setPaint(Color.blue.brighter());
        g2.draw(p);
        g2.dispose();
    }
}
Community
  • 1
  • 1
mKorbel
  • 109,525
  • 20
  • 134
  • 319
2

Maybe you could use a JComboBox like they do in this example. If not, it might give you an idea for how to do it with a JList.

Community
  • 1
  • 1
Catalina Island
  • 7,027
  • 2
  • 23
  • 42
  • That is actually quite clean and simple, your reply got me thinking that implementing the whole thing myself is maybe not such a bad idea after all... thanks! – andli Apr 24 '12 at 13:51
2

Use a JTree instead. It easily supports groups as branches, while items are leaves. See How to Use Trees for more details and screen-shots like..

DynamicTreeDemo

The renderer can adjust the look to need.


As noted by @Robin:

I think the single JTree as well as a single JList with a custom renderer both have the same shortcoming: all nodes are selectable.

The branch nodes are selectable in order to open & close them, so I see no problem with them being selectable. It is a slightly different user action to the list with 'headings'. There are disadvantages to using a tree (e.g. having to tab past branch groups, & advantages also (e.g. the branches are closed by default to give access to the groups with fewer keystrokes/clicks).

Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
  • 1
    I think the single `JTree` as well as a single `JList` with a custom renderer both have the same shortcoming: all nodes are selectable. I am not aware of any API to disable selecting of certain nodes. And a Solaris screenshot ... now I know why you are fan of OS X screenshots :-) – Robin Apr 24 '12 at 13:30
  • Yes I considered a JTree, but as you can see in the question I have ruled it out due to usability and design concerns. It is my last resort. :) – andli Apr 24 '12 at 13:49
  • 2
    Regarding disabling selection for some paths: you'll need a custom selection model. See for a tree example: https://forums.oracle.com/forums/thread.jspa?threadID=1380353&tstart=3&messageID=6355951 – Walter Laan Apr 24 '12 at 14:18
2

You could make your own implementation of a ListCellRenderer to fit for your needs. Just let it check if the item it is rendering is a parent node or a child node.

To not make the parent categories selectable in the list is a bit more tricky. You can modify the renderer to not display if the parent category is selected, or perhaps override the default behavior of the jlist.

Personally? I'd go for the JTree, you can make it look exactly the same as your list, but it adds a lot of other abilities, like 3 levels of depth, it just seems more flexible.

You have 2 reasons not to use a jtree.

usability: you can modify the jtree to act the way you want just like you can with the list

design: i dont think its a better design to store a tree (categories and subcategories are trees) in a flat list.

Maarten Blokker
  • 604
  • 1
  • 5
  • 23