JavaSwingJList and ListModel: Layout orientation


Layout orientation

Real horizontal layout

(just the way vertical is done)

The cells should be layed out at their preferred width (not all at the maximum value) and the fixed cell width. cells' heights show grow to fill the JList.

I don't understand why this isn't possible. It could be done completely analogous to the vertical layout (only a bit more complicated due to component orientation) but it is much simpler than the wrap modes.

It cannot be done with a wrap mode with visibleRowCount == 1 because then a) the cells don't fill the list vertically, b) all cells are given the same width (at least in the BasicListUI implementation), c) the Scrollable semantics are different (but could be changed).

Flowing with one fixed dimension

Flowing (i.e. variable number of rows/columns) in general is not done easily unless one dimension is fixed. Therefore in the following it is assumed that the visibleRowCount has been set to a value>0, therefore fixing the height (calculated from fixed cell height or by "some algorithm" from the renderer's heights). The existing wrap modes only the height is allowed to be fixed, not the width (see below).

The visible row count in VERTICAL mode is used with a different meaning (in a way the opposite - before it was only related to when scrollbars would appear, but had no effect on layout, now it determines the layout). Maybe there should have been an extra property for it.

Scrollable

getPreferredScrollableViewportSize is incorrect: It returns the preferred size. Thus the scroll pane would always appear large enough to show all columns. (for the height, this is would be acceptable if one assumes that only one of horizontal/vertical scrolling should be active, but it fails with the correction of the row count).

There should be something setVisibleColumnCount to set the column value, although this could only calculate some approximation if the widths were different.

Here is a fix: Note it will not change anything with visibleRowCount <= 0

import javax.swing.JList;
import javax.swing.ListModel;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Insets;



public class FList
    extends JList
{
    private int visibleColumnCount = 3;


    public FList(ListModel data)
    {
        super(data);
    }


    /** value used to determine the preferredScrollableViewportSize.
        default: 3 */
    public final int getVisibleColumnCount()
    {
        return visibleColumnCount;
    }

    public void setVisibleColumnCount(int value)
    {
        if (visibleColumnCount == value)
            return;

        int old = visibleColumnCount;

        visibleColumnCount = value;

        firePropertyChange("visibleColumnCount", old, visibleColumnCount);
    }


    public Dimension getPreferredScrollableViewportSize()
    {
        if (getLayoutOrientation() == VERTICAL)
            return super.getPreferredScrollableViewportSize();

        if (getVisibleRowCount() <= 0)
            return super.getPreferredScrollableViewportSize();

        Dimension result;

        Insets n = getInsets();

        if (getModel().getSize() == 0)
        {
            result = new Dimension();

            int width = getFixedCellWidth();

            if (width == -1)
                width = 256; // broken, but the same way as super

            int height = getFixedCellHeight();

            if (height == -1)
                height = 16; // ditto

            result.width = width * visibleColumnCount + n.left + n.right;
            result.height = height * getVisibleRowCount() + n.top + n.bottom;
        }
        else
        {
            // not: getPreferredSize()
            result = new Dimension(getUI().getPreferredSize(this));

            // assumes all cells will get the same size
            // or the first cell is a typical example
            Rectangle r = getCellBounds(0, 0);

            result.width = r.width * visibleColumnCount + n.left + n.right;
            // see below (correction): if this is used, the call to getPreferredSize
            // above is not needed
            // result.height = r.height * getVisibleRowCount() + n.top + n.bottom;
        }

        return result;
    }
}








getScrollableTracksViewportWidth/Height seems correct (only track if underflowing)

getScrollableUnit/BlockIncrement are complicated. These could well be delegated to the LAF, which now alone knows how it lays out?

getFirst/LastVisibleIndex: same; seem to have (had) problems with too large lists.

Preferred size (BasicListUI)

minimum/maximum size = preferred size

All columns get the same width (there is nothing that requires this): the fixedCellWidth or the maximum of the renderer's widths. All columns get the same height: the fixedCellHeight, or the maximum of the renderer's heights.

The preferred size is

(insets.left + insets.right + columnCount * cellWidth, insets.top + insets.bottom + rowCount * cellWidth)

where columnCount is determined from the number of items and the rowCount, and rowCount is corrected the visibleRowCount (or, not considered here, determined from the list's height (recursive dependency)).

The width could be calculated for each column individually, but that may be undesirable: changes will possibly cause major changes in the widths.

Correction

The rowCount is corrected to fill columns more equally. Combined with the broken getPreferredScrollableViewportSize this is quite weird as addition/removal of a single item will also cause the height to change and possibly lead to scroll bars in both directions:

import javax.swing.*;


public class L2
{
    public static void main(String[] args)
    {
        final DefaultListModel data = new DefaultListModel();

        final int count = args.length > 0 ? Integer.parseInt(args[0]) : 16;

        for (int i = 0; i < count; i++)
            data.addElement(String.valueOf(i));

        JList l = new JList(data);

        l.setVisibleRowCount(5);
        l.setLayoutOrientation(l.HORIZONTAL_WRAP);

        JScrollPane p = new JScrollPane(l);

        JButton b = new JButton(new AbstractAction("Add")
        {
            private int next = count;

            public void actionPerformed(java.awt.event.ActionEvent e)
            {
                data.addElement(String.valueOf(next));

                next++;
            }
        });
        
        JFrame f = new JFrame();

        f.getContentPane().setLayout(new java.awt.FlowLayout());
        f.getContentPane().add(p);

        f.getContentPane().add(b);

        f.setSize(500, 400);
        f.show();
    }
}

Initially, the 16 items are distributed evenly over the 4 columns (not 3x5 + 1). After the addition of one item, their are now layed out as (3x5 + 2). Thus the preferred height changes. By the trivial implementation of getPreferredScrollableViewportSize, this would affect the viewport size, but as JScrollPane isValidateRoot, it stays at its old size and is not layed out at its new preferred size (FlowLayout is used so that the JScrollPane is initially at its preferred size). Therefore the horizontal scroll bars appear.

This would be alright if getPreferredScrollableViewportSize would really have the height that fit for any number of items, i.e. its height would really be based on the visibleRowCount and not on the corrected number of rows. This can be fixed (see FList code above, where it is commented out).

Note that if the height changed because of a different calculated cell height, the scroll bars would also appear. The isValidateRoot of JScrollPane seems to be overly aggressive in keeping the JScrollPane at the same size. Anyway, to avoid jumping around of components, the preferred scrollable viewport size should not change needlessly. But this leads far into JScrollPane structure.

Layout (BasicListUI)

The cells are just layed out at the calculated width/height from above, starting from left/right top. The heights and widths are not adjusted.

(In VERTICAL mode, the width is grown to the whole width of the JList).

Thus if the JList is wider/higher that preferred, the extra region will just be empty space.

Having the whole renderer cell selected looks weird for a less wide item. The renderer could be adjusted to only paint the selection around the text part (i.e. only grow to its preferred width).

getCellBounds now really would be a union of rectangles, not a single rectangle, for most arguments. There seems to be no documentation what it is then.

No wrap modes with fixed column count

Both HORIZONTAL_WRAP and VERTICAL_WRAP keep the row count fixed with a variable number of columns. There should be wrap modes that keep the column count fixed with a variable number of rows (this would go together with a width/height transposed version of getScrollableViewportSize above if it isn't symmetric already).

With visibleRowCount > 0, the wrap modes seem to behave the same except for the item distribution order on the available cells.

Problems of fixed dimension (here: vertical)

If there are enough items, no matter what the vertical size is, there will be a fixed number of rows (determined by visibleRowCount and the current number of items alone). Thus either space is wasted (if the list is larger) or vertical scroll bars appear if the viewport height does not match exactly the preferred height. Making the list dynamically adjust to the vertical space that is available is only possible with the far more complicated free flowing.

The correction of the visibleRowCount will also ignore that there currently is space for another row (though that doesn't matter as it does not affect the number of columns).

Results

Two of the four possible wrap modes are missing: there is no way to fix the number of columns (except, possibly, dynamically adjusting the number of rows to the current item count) instead of the number of rows. The modes actually split into two unrelated notions: a) distribution order of the items and b) whether the row count or column count is fixed (or taken as a starting point with free flowing).

The implementation of getPreferredScrollableViewportSize is disastrous. A fix is provided.

Implementations of the Scrollable features are slightly hindered by no knowledge of the layout algorithm in detail and by the lack of a defined cell size if there are no items and no fixedCell sizes.

The double usage of visibleRowCount (or, hypothetically, visibleColumnCount) for both determining the number of rows (resp. columns) and the corresponding viewport size may be justified because that way, only scroll bars in one direction will appear.

The modes with item count fixed in one direction are unsatisfying unless the height (resp. width) of the list is tightly controlled.

Flowing with both dimensions free

"It's not that simple."

These are just some thoughts, the problem really should be treated more generally.

If the visible row count is <= 0, the list will guess a number of columns (horizontal wrap) or rows (horizontal wrap). If as usual, the list first has size (0, 0), these will turn out to be 1.

As a) getScrollablePreferredViewportSize will just return the preferred size, b) the visible row count obviously isn't set to anything usable, the list will appear without scrollbars if the preferred size is actually used for layout. Thus it will appear very wide/high.

In vertical wrap, it will track the viewport height (in horizontal, the width). Thus here, in horizontal mode really vertical scroll bars would appear (unlike visibleRowCount > 0, where vertical scroll bars typically never appear and both wrap modes grow horizontally). IMO both modes should have guessed the row count to be similar to the case with visibleRowCount > 0, and the two other (non-existing) modes would guess the height (or take the visibleColumnCount).

Anyway there would have to be a different way to specify the initial viewport size (in fact here visibleRowCount is misused as a flag to turn this mode on).

Furthermore, this guessing is bad especially for vertical wrap where a minor increase in the height will cause the rowCount to be 2 and then the right half of the list suddenly is empty (in horizontal wrap mode this does not feel that bad) because its viewport originally was given the previous size and usually doesn't change easily (this is a problem of the original setup).


(C) 2001-2009 Christian Kaufhold (swing@chka.de)