//@+leo-ver=4
//@+node:@file src/ControlListBox.java
//@@language java

//@+others
//@+node:imports
import waba.ui.*;
import waba.sys.*;
import waba.fx.*;
import waba.util.*;

//import waba.lang.*;
import java.lang.*;

//@-node:imports
//@+node:class ControlListBox
/**
 * A ListBox-like container which allows adding of
 * arbitrary non-homogenous controls (instead of text strings).
 * <br><br>
 * ControlListBox is 'yet another' implementation
 * of a scrollable container, this time in the
 * spirit of a ListBox which allows the addition
 * of arbitrary controls.
 * <br><br>
 * As the name 'listbox' implies, the intention is that
 * items get stacked vertically. It's meant to look
 * and feel a bit like a listbox, except that it allows
 * virtually any Control or subclass to be added.
 * <br><br>
 * So far, there is no support for horizontal scrolling,
 * which puts the onus on the programmer to ensure that
 * the added items' width does not exceed this control's
 * width.
 * <br><br>
 * Scrollbar is initially invisible, and becomes visible
 * only when needed.
 * Scrolling strategy is pixel-oriented, not item-oriented.
 * But you can invoke setScrollLineAmount to change the
 * step scroll amount.
 * <br><br>
 * This is a work in progress - no support exists yet
 * for highlighting, finding or removing items.
 * <br><br>
 * You should have received a file called 'Test.java'
 * along with this file. Test.java provides a very
 * clear demonstration.
 * <br><br>
 * The anatomy of this control is an outer container holding
 * a viewport container and a scrollbar. The viewport container
 * holds an inner content container, to which the child widgets
 * are added.
 * @author David McNab
 */

public class ControlListBox extends Container
{
    //@    @+others
    //@+node:attribs
    // our 'viewport' Container acts as a
    // clipping rectangle for positioning of content
    public Container viewport;
    
    // current dimensions of this viewport
    int wViewport, hViewport;
    
    // content Container, sctually holds the child controls
    // and sits inside the viewport Container
    public Container content;
    
    // position and size of content Container
    // relative to viewport Container
    protected int xContent;
    protected int yContent;
    protected int wContent;
    protected int hContent;
    
    // gotta have a ScrollBar
    public ScrollBar scrollbar;
    
    // store width of this ScrollBar
    protected int wScrollbar;
    
    protected boolean scrollbarShowing = false;
    
    // designated line scroll increment, in pixels
    protected int scrollLineAmount = 12;
    
    protected Vector childControls;
    
    protected int numChildControls;
    
    //@-node:attribs
    //@+node:ControlListBox
    /** Creates a ControlListBox widget
     * <br><br>
     * After creating this widget, you should setRect() it
     */
    
    public ControlListBox()
    {
        super();
    
        // start with sane defaults
        Rect r = new Rect();
        r.x = 10;
        r.y = 10;
        r.width = 60;
        r.height = 60;
    
        // apply this initial shape
        super.setRect(r);
    
        // create and size the viewport
        viewport = new Container();
        wViewport = r.width-2;
        hViewport = r.height-2;
        viewport.setRect(1, 1, wViewport, hViewport);
    
        // create and size the content container
        content = new Container();
        content.setRect(0, 0, wViewport, 0);
    
        // and set tracking attributes for content container
        xContent = 0;
        yContent = 0;
        wContent = wViewport;
        hContent = 0;
    
        // create scrollbar, initially hidden
        scrollbar = new ScrollBar();
        wScrollbar = scrollbar.getPreferredWidth();
        scrollbar.setRect(0,0,0,0);
    
        // and assemble the final widget
        viewport.add(content);
        super.add(viewport);
        super.add(scrollbar);
    
        // cosmetic stuff
        setBorderStyle(BORDER_SIMPLE);
    
        // keep track of the child controls we add
        childControls = new Vector();
        numChildControls = 0;
    
    }
    //@-node:ControlListBox
    //@+node:setRect
    /** Adjusts size and placement of this widget, and
     * updates the inner structure as needed
     */
    
    public void setRect(int x, int y, int w, int h)
    {
        super.setRect(x, y, w, h);
    
        // adjust content and viewport dimension attributes
        wContent = wViewport = w - 4;
        hViewport = h - 4;
    
        // resize viewport and scrollbar
        updatePanes();
    
        // and resize content pane
        Rect rContent = content.getRect();
        rContent.width = wContent;
        content.setRect(rContent);
    }
    
    //@-node:setRect
    //@+node:setRect
    /** Adjusts size and placement of this widget, and
     * updates the inner structure as needed
     */
    
    public void setRect(Rect r)
    {
        // delegate to other override where it all happens
        setRect(r.x, r.y, r.width, r.height);
    }
    
    //@-node:setRect
    //@+node:add(Control,int,int)
    /** Dummy override that just passes to add(Control)
     * Both arguments will be ignored.
     */
    
    public void add(Control ctrl, int x, int y)
    {
        // ditch the x and y attribs
        add(ctrl);
    }
    
    //@-node:add(Control,int,int)
    //@+node:add(Control)
    /** Add a single control to this widget, sizing the control
     * to its preferred width/height
     * <br><br>
     * If you really want to enforce a size for the control, use
     * the addSized() method instead.
     *
     * @param ctrl almost any widget object with Control in its pedigree
     */
    
    public void add(Control ctrl)
    {
        // default to the new control's preferred sizing
        addSized(ctrl, ctrl.getPreferredWidth(), ctrl.getPreferredHeight());
    }
    
    //@-node:add(Control)
    //@+node:addSized
    /** add a Control to this widget, explicitly setting its width
     * and height
     * @param ctrl the control to add
     * @param w width to set for the control
     * @param h height to set for the control
     */
    
    public void addSized(Control ctrl, int w, int h)
    {
        // remember previous content pane height
        int hOld = hContent;
    
        // set added control's rectangle
        ctrl.setRect(0, hOld, w, h);
    
        // expand the content to fit new widget
        hContent += h;
    
        // narrow the viewport and make space for
        // scrollbar, if needed
        updateScrollbar();
    
        // update and resize content pane
        content.setRect(xContent, yContent, wContent, hContent);
        content.add(ctrl);
    
        // and, of course, add it to our internal list
        childControls.add(ctrl);
        numChildControls++;
    }
    
    //@-node:addSized
    //@+node:find
    /** Search for child control c, returning its
     * index if it is in fact a child control, or
     * -1 if it is not present on this widget
     */
    
    public int find(Control c)
    {
        return childControls.find(c);
    }
    
    //@-node:find
    //@+node:remove
    /** Remove the control at the given index
     * @param idx the integer index at which this control was added
     */
    
    public void remove(int idx)
    {
        int i;
        Rect r;
        int hCtrl;
        Control ctrl;
        Object[] ctrls;
        Control c;
    
        // sanity check
        if (idx < 0 || idx >= numChildControls)
            return;
    
        // get the control in question
        ctrl = (Control)childControls.items[idx];
    
        // how high is this control?
        hCtrl = ctrl.getRect().height;
    
        // firstly, drop the ctrl from our own list
        childControls.del(idx);
    
        // and delete from content pane
        content.remove(ctrl);
    
        // and update our count
        numChildControls--;
    
        // and shift remaining controls up
        shiftContents(idx, -hCtrl);
    }
    
    //@-node:remove
    //@+node:remove
    /** Remove a control from this widget */
    
    public void remove(Control c)
    {
        int idx = childControls.find(c);
        if (idx >= 0)
            remove(idx);
    }
    
    //@-node:remove
    //@+node:insert
    /** Inserts a control into this widget at given index */
    
    public void insert(int idx, Control c, int height)
    {
        // sanity check
        if (idx < 0 || idx >= numChildControls)
            return;
        
        // inserting at end is just an add
        if (idx == numChildControls - 1)
        {
            add(c);
            return;
        }
        
        // we need a real insert
        
        // get required y val
        int yNew = ((Control)childControls.items[idx]).getRect().y;
    
        // insert into our tracking vector
        childControls.insert(idx, c);
        numChildControls++;
    
        // move controls down to make room
        shiftContents(idx+1, height);
    
        // size and place this new control
        c.setRect(0, yNew, wContent, height);
        content.add(c);
            
    }
    
    
    //@-node:insert
    //@+node:insert
    public void insert(int idx, Control c)
    {
        insert(idx, c, c.getPreferredHeight());
    }
    
    //@-node:insert
    //@+node:resizeChild
    /** changes the height of a child control
     */
    
    public void resizeControl(Control c, int newHeight)
    {
        int idx = childControls.find(c);
        if (idx >= 0)
            resizeChild(idx, newHeight);
    }
    
    //@-node:resizeChild
    //@+node:resizeChild
    /** changes the height of a child control */
    
    public void resizeChild(int idx, int newHeight)
    {
        // sanity check
        if (idx < 0 || idx >= numChildControls)
            return;
    
        Control c = (Control)childControls.items[idx];
        Rect r = c.getRect();
        
        // determine height difference
        int dH = newHeight - r.height;
    
        // resize control
        r.height  = newHeight;
        c.setRect(r);
        
        // and move the controls underneath accordingly
        shiftContents(idx+1, dH);
    }
    
    //@-node:resizeChild
    //@+node:shiftContents
    protected void shiftContents(int idx, int dy)
    {
        Control c;
        Rect r;
    
        // now get the fast-access array to remaining controls
        Object[] ctrls = childControls.items;
    
        // move all the controls
        for (int i=idx; i<numChildControls; i++)
        {
            // get the control
            c = (Control)ctrls[i];
    
            // fetch control's rect
            r = c.getRect();
    
            // bump it up by height of deleted control
            r.y += dy;
    
            // and rewrite to control
            c.setRect(r);
        }
    
        // lastly, resize the content container
        r = content.getRect();
        r.height += dy;
        r.width = wViewport;
        content.setRect(r);
        hContent += dy;
    
        // and update scrollbars
        updateScrollbar();
    }
    
    //@-node:shiftContents
    //@+node:scroll
    /** Scroll the viewable area by a given number of pixels.
     * Negative scroll arg means view items higher on the list
     * @param dy Number of pixels to scroll
     */
    
    public void scroll(int dy)
    {
        int y0new, y1new, dy1;
    
        // sanity check - no scrolling if content pane fits wholly
        // within view pane
        if (hContent < hViewport)
            return;
    
        // handle scroll up (ie, viewing earlier items)
        if (dy < 0)
        {
            dy = max(dy, yContent);
        }
    
        // handle scroll down (ie, viewing later items)
        else if (dy > 0)
        {
            dy = min(dy, (yContent+hContent) - hViewport);
        }
    
        // bail on trivial non-action
        if (dy == 0)
            return;
    
        // shift the content container    
        yContent -= dy;
        content.setRect(xContent, yContent, wContent, hContent);
        
        // and update the scrollbar
        scrollbar.setValue(-yContent);
    }
    
    
    //@-node:scroll
    //@+node:scrollUpFully
    /** Scroll all the way to the top
     */
    
    public void scrollUpFully()
    {
        scroll(yContent);
    }
    
    //@-node:scrollUpFully
    //@+node:scrollUpPage
    /** Scroll up one page (less a line's worth)
     */
    
    public void scrollUpPage()
    {
        scroll(-(hViewport - scrollLineAmount));
    }
    //@nonl
    //@-node:scrollUpPage
    //@+node:scrollUpLine
    /** Scroll up one line (using the parameter given
     * in the last call to setScrollLineAmount)
     */
    
    public void scrollUpLine()
    {
        scroll(-scrollLineAmount);
    }
    
    //@-node:scrollUpLine
    //@+node:scrollDownLine
    /** Scroll down one line (using the parameter given
     * in the last call to setScrollLineAmount)
     */
    
    public void scrollDownLine()
    {
        scroll(scrollLineAmount);
    }
    
    //@-node:scrollDownLine
    //@+node:scrollDownPage
    /** Scroll down one page (less a line's worth)
     */
    
    public void scrollDownPage()
    {
        scroll(hViewport - scrollLineAmount);
    }
    
    //@-node:scrollDownPage
    //@+node:scrollDownFully
    /** Scroll all the way to the bottom
     */
    
    public void scrollDownFully()
    {
        scroll((yContent+hContent) - hViewport);
    }
    
    //@-node:scrollDownFully
    //@+node:setScrollLineAmount
    /** Set the number of pixels by which the control should
     * line-scroll. Setting this to 11 gets accurate scrolling
     * if the widget is populated only by Label controls/
     *
     * Initially the widget is set to 10 pixels per line scroll
     *
     * @param numPixels the number of pixels per line, will be set
     * to 1 if <= 0
     */
    
    public void setScrollLineAmount(int numPixels)
    {
        scrollLineAmount = max(1, numPixels);
    }
    
    //@-node:setScrollLineAmount
    //@+node:max
    int max(int n1, int n2)
    {
        return (n1 > n2) ? n1 : n2;
    }
    
    //@-node:max
    //@+node:min
    int min(int n1, int n2)
    {
        return (n1 < n2) ? n1 : n2;
    }
    
    
    //@-node:min
    //@+node:updatePanes
    /** shows or hides the scrollbar, ensuring that if
     * the scrollbar is displayed, it is updated
     */
    protected void updatePanes()
    {
        // set up viewport rect
        Rect rViewport = getRect();
        rViewport.x = 2;
        rViewport.y = 2;
        rViewport.height -= 4;
    
        if (scrollbarShowing)
        {
            // show scrollbar, shrink viewport
            rViewport.width -= 4 + wScrollbar;
    
            scrollbar.setRect(
                rViewport.x + rViewport.width, 1,
                wScrollbar, rViewport.height+2);
        }
    
        else
        {
            // gotta hide it, and expand viewport
            rViewport.width -= 4;
            scrollbar.setRect(0, 0, 0, 0);
        }
    
        // and apply to viewport
        viewport.setRect(rViewport);
    
    }
    
    //@-node:updatePanes
    //@+node:updateScrollbar
    /** Update scrollbar state if required
     */
    
    protected void updateScrollbar()
    {
        if (hContent > hViewport)
        {
            // update scrollbar
            scrollbar.setVisibleItems(hViewport);
            scrollbar.setMaximum(hContent);
            scrollbar.setBlockIncrement(hViewport - scrollLineAmount);
            scrollbar.setUnitIncrement(scrollLineAmount);
        }
    
        // bail if no action is needed
        if ((hContent <= hViewport) ^ scrollbarShowing)
            return;
    
        // otherwise adjust the viewport and scrollbar geometry
        scrollbarShowing = !scrollbarShowing;
        updatePanes();
    }
    
    //@-node:updateScrollbar
    //@+node:onEvent
    /** Called by system, to handle scrollbar events */
    
    public void onEvent(Event e)
    {
        if (e.type == ControlEvent.PRESSED && e.target == scrollbar)
        {
            // shift the content container according to scrollbar state
            yContent = -scrollbar.getValue();
            content.setRect(xContent, yContent, wContent, hContent);
        }
    }
    
    //@-node:onEvent
    //@+node:debug
    void debug(String msg)
    {
        // nothing to see here, move along please
        new MessageBox("debug", msg).popupBlockingModal();
    }
    
    //@-node:debug
    //@+node:debugPopup
    public void debugPopup()
    {
        debug("hCont="+hContent+" hView="+hViewport);
    }
    
    //@-node:debugPopup
    //@-others
}
//@-node:class ControlListBox
//@-others

//@-node:@file src/ControlListBox.java
//@-leo
