swingn.ui.ListBox.java Source code

Java tutorial

Introduction

Here is the source code for swingn.ui.ListBox.java

Source

//
// SwingN - swing-like utilities and widgets for use with tripleplay
// Copyright (c) 2014, Cupric - All rights reserved.
// http://github.com/cupric/swingn/blob/master/LICENSE

package swingn.ui;

import java.util.Arrays;
import java.util.Map;

import pythagoras.f.Dimension;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;

import react.Connection;
import react.Slot;
import react.Value;
import swingn.ui.ListModel.Event;
import tripleplay.ui.Element;
import tripleplay.ui.Elements;
import tripleplay.ui.Layout;
import tripleplay.ui.Selector;
import tripleplay.ui.ToggleButton;
import tripleplay.ui.layout.AxisLayout;
import tripleplay.util.Objects;

/**
 * Simple {@code Elements} implementation associating each of its children with a provided object.
 * For example, each widget in a list could be associated with a command object that is dispatched
 * to a game control mechanism.
 * <p>{@link ToggleButton} is the canonical list box widget. Unlike swing, the individual widgets
 * contained in the list box perform their own pointer handling and selection logic. Furthermore,
 * scrolling is not handled at all within this implementation. Typically, a {@code ListBox} is
 * set as the content of a {@link tripleplay.ui.Scroller} instance, but this is not assumed
 * or required.</p>
 * TODO: integrate with scrolling somehow
 * TODO: optimize allocation of elements (for large lists)
 * TODO: multiple selection?
 * TODO: drag outside elements to change selection
 * TODO: special pointer/mouse support
 * @param <I> the type of object associated with each list box child element
 */
public class ListBox<I> extends Elements<ListBox<I>> {
    /**
     * Creates an item renderer that, for a source item, produces a {@link ToggleButton} with text
     * set to the result of the given function call on the item.
     */
    public static <T> Renderer<T> labels(final Function<? super T, String> fn) {
        return new Renderer<T>() {
            public Element<?> create(Element<?> previous, T item) {
                if (previous == null) {
                    previous = new ToggleButton(fn.apply(item));
                } else {
                    ((ToggleButton) previous).text.update(fn.apply(item));
                }
                return previous;
            }
        };
    }

    /**
     * Creates an item renderer that, for a source item, produces a {@link ToggleButton} with text
     * set to the result of the source item's {@code toString()} method.
     */
    public static <T> Renderer<T> labels() {
        return ListBox.<T>labels(Functions.toStringFunction());
    }

    /**
     * Creates a list box of {@link ToggleButton} elements, each of which is associated with
     * its own text.
     */
    public static ListBox<String> ofStrings(Layout layout) {
        Renderer<String> labels = labels();
        return new ListBox<String>(labels, layout);
    }

    /**
     * Creates a new list box with the given item renderer and a vertical layout.
     */
    public static <I> ListBox<I> create(Renderer<I> renderer) {
        return new ListBox<I>(renderer);
    }

    /**
     * Creates a new list box with the given item renderer and layout.
     */
    public static <I> ListBox<I> create(Renderer<I> renderer, Layout layout) {
        return new ListBox<I>(renderer, layout);
    }

    public final Selector selector = new Selector(this, null);

    /**
     * Renders a list box item into an {@code Element} instance.
     * @param <I> the type of the item the renderer handles
     */
    public interface Renderer<I> {
        /**
         * Creates a new element from the given item. If {@code previous} is not null, the
         * implementation may choose to repopulate and return it rather than creating a new
         * element.
         * <p>NOTE: the returned element must provide its own pointer handling and selection
         * logic, so that it may be connected properly to the {@link Selector}. See
         * {@link ToggleButton} for how this may be achieved.</p>
         * @param item the object to be rendered
         * @param previous a previously created element no longer in the list box, may be null
         * @return an {@code Element} configured to display the item, or a repopulated
         * {@code previous} if appropriate
         */
        Element<?> create(Element<?> previous, I item);
    }

    /**
     * Creates a new list box with the given item renderer and a vertical layout with no gaps.
     */
    public ListBox(Renderer<I> renderer) {
        this(renderer, AxisLayout.vertical().gap(0));
    }

    /**
     * Creates a new list box with the given item renderer and layout.
     */
    public ListBox(Renderer<I> renderer, Layout layout) {
        super(layout);
        _renderer = Preconditions.checkNotNull(renderer);
        selector.selected.connect(new Slot<Element<?>>() {
            @Override
            public void onEmit(Element<?> event) {
                _selectedItem.update(_itemMap.get(event));
            }
        });
        _selectedItem.connect(new Slot<I>() {
            @Override
            public void onEmit(I event) {
                Element<?> sel = null;
                if (event != null) {
                    for (Element<?> child : ListBox.this) {
                        if (Objects.equal(_itemMap.get(child), event)) {
                            sel = child;
                        }
                    }
                }
                selector.selected.update(sel);
            }
        });
    }

    /**
     * Sets up this list box to track the contents of the given model. Note that any subsequent
     * attempts to modify the list box contents will have undefined effects.
     */
    public void connect(final ListModel model) {
        // TODO: does ListModel need to be more fleshed out as a member?
        // TODO: allow garbage collection of the list box sooner
        final Connection[] conn = { null };

        final Slot<ListModel.Event> eventSlot = new Slot<Event>() {
            @Override
            public void onEmit(Event event) {
                if (!isAdded()) {
                    // if we are no longer in the hierarchy and get a model change, disconnect
                    // the signal so the list box can be garbage collected
                    // TODO: allow GC sooner?
                    if (conn[0] != null) {
                        conn[0].disconnect();
                        conn[0] = null;
                        return;
                    }
                }
                updateItems(event);
            }
        };

        Slot<Boolean> addRemove = new Slot<Boolean>() {
            @Override
            public void onEmit(Boolean event) {
                if (event) {
                    if (conn[0] == null) {
                        setItems(model);
                        conn[0] = model.onChange().connect(eventSlot);
                    }
                }
            }
        };

        hierarchyChanged().connect(addRemove);
        addRemove.onEmit(isAdded());
    }

    /**
     * Gets the index of an item in the list box.
     */
    public int indexOf(I item) {
        for (int ii = 0, nn = childCount(); ii < nn; ii++) {
            if (_itemMap.get(childAt(ii)).equals(item)) {
                return ii;
            }
        }
        return -1;
    }

    /**
     * Gets the item at the given position.
     */
    public I itemAt(int index) {
        return _itemMap.get(childAt(index));
    }

    /**
     * Gets the renderer used for this list box.
     */
    public Renderer<I> renderer() {
        return _renderer;
    }

    /**
     * Gets the value containing the currently selected item. This will be updated when some child
     * element is selected with the child's associated item.
     */
    public Value<I> selected() {
        return _selectedItem;
    }

    /**
     * Clears the selection such that {@code selected().get()} is null.
     */
    public void clearSelection() {
        selector.selected.update(null);
    }

    /**
     * Sets the content of the list to the given items, rendering if appropriate.
     */
    public void setItems(I... items) {
        setItems(Arrays.asList(items));
    }

    /**
     * Sets the content of the list to the given items, rendering if appropriate.
     */
    public void setItems(Iterable<I> items) {
        removeAll();
        Preconditions.checkState(_itemMap.isEmpty());
        for (I item : items) {
            Element<?> elem = _renderer.create(null, item);
            add(elem);
            _itemMap.put(elem, item);
        }
    }

    /**
     * Adds an item to the list.
     */
    public void addItem(I item) {
        Element<?> elem = _renderer.create(null, Preconditions.checkNotNull(item));
        add(elem);
        _itemMap.put(elem, item);
    }

    /**
     * Inserts an item into the list at a given index.
     */
    public void addItem(int idx, I item) {
        Element<?> elem = _renderer.create(null, Preconditions.checkNotNull(item));
        add(idx, elem);
        _itemMap.put(elem, item);
    }

    /**
     * Removes the item at the given index.
     */
    public void removeItem(int idx) {
        Element<?> elem = childAt(idx);
        if (selector.selected.get() == elem) {
            selector.selected.update(null);
        }
        destroyAt(idx);
    }

    /**
     * Provides a minimum width to be used when laying out the list box.
     */
    public ListBox<I> setMinWidth(float minWidth) {
        _minWidth = minWidth;
        invalidate();
        return this;
    }

    @Override
    protected LayoutData createLayoutData(float hintX, float hintY) {
        return new SizableLayoutData(super.createLayoutData(hintX, hintY),
                new Dimension(_minWidth != null ? _minWidth : 0, 0)).forWidth(Take.MAX);
    }

    @Override
    protected Class<?> getStyleClass() {
        return ListBox.class;
    }

    @Override
    protected void didRemove(Element<?> child, boolean destroy) {
        super.didRemove(child, destroy);
        _itemMap.remove(child);
    }

    protected void setItems(ListModel model) {
        destroyAll();
        for (int ii = 0, nn = model.getSize(); ii < nn; ii++) {
            @SuppressWarnings("unchecked")
            I obj = (I) model.getElementAt(ii);
            addItem(obj);
        }
    }

    protected void updateItems(ListModel.Event event) {
        int i0 = event.getIndex0(), i1 = event.getIndex1();
        switch (event.getType()) {
        case CONTENTS_CHANGED:
            for (int idx = i0; idx <= i1; idx++) {
                @SuppressWarnings("unchecked")
                I obj = (I) event.getSource().getElementAt(idx);
                Element<?> elem = childAt(idx);
                _renderer.create(elem, obj);
                _itemMap.put(elem, obj);
            }
            break;
        case INTERVAL_ADDED:
            for (int idx = i0; idx <= i1; idx++) {
                @SuppressWarnings("unchecked")
                I obj = (I) event.getSource().getElementAt(idx);
                addItem(idx, obj);
            }
            break;
        case INTERVAL_REMOVED:
            for (int idx = i1; idx >= i0; idx--) {
                removeItem(idx);
            }
            break;
        }
    }

    protected final Renderer<I> _renderer;
    protected final Value<I> _selectedItem = Value.create(null);
    protected final Map<Element<?>, I> _itemMap = Maps.newHashMap();
    protected Float _minWidth;
}