view mde/gui/widget/layout.d @ 103:42e241e7be3e

ContentList content type; getting content items/lists from Options generically via content.Items, and a new addContent widget function. Several improvements to generic handling of content. New button-with-text widget. Some tidy-up. Some name changes, to increase uniformity. Bug-fix: floating widgets of fixed size could previously be made larger than intended from config dimdata.
author Diggory Hardy <diggory.hardy@gmail.com>
date Tue, 25 Nov 2008 18:01:44 +0000
parents 5de5810e3516
children 08651e8a8c51
line wrap: on
line source

/* LICENSE BLOCK
Part of mde: a Modular D game-oriented Engine
Copyright © 2007-2008 Diggory Hardy

This program is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either
version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>. */

/// Gui layout widgets.
module mde.gui.widget.layout;

import mde.gui.widget.Widget;
import mde.gui.exception;

import mde.gui.content.Content;

import tango.util.container.HashMap;

debug {
    import tango.util.log.Log : Log, Logger;
    private Logger logger;
    static this () {
        logger = Log.getLogger ("mde.gui.widget.layout");
    }
}

/*************************************************************************************************
 * Encapsulates a grid of Widgets.
 *
 * Currently there is no support for changing number of cells, sub-widgets or sub-widget properties
 * (namely isW/HSizable and minimal size) after this() has run.
 *
 * Since a grid with either dimension zero is not useful, there must be at least one sub-widget.
 *
 * The grid has no border but has spacing between widgets.
 *************************************************************************************************/
class GridLayoutWidget : GridWidget
{
    /** Constructor for a grid layout widget.
     *
     * Widget uses the initialisation data:
     * ---
     * ints = [widget_type, align_flags, rows, cols]
     * // or with column widths and row heights:
     * ints = [widget_type, align_flags, rows, cols, col1width, ..., colCwidth, row1height, ..., rowRheight]
     * strings = [w11, w12, ..., w1C, ..., wR1, ..., wRC]
     * ---
     * where R and C are the number of rows and columns, and wij is the ID (from parent Window's
     * list) for the widget in row i and column j. The number of parameters must be r*c + 3.
     * 
     * The content parameter is passed on to all children accepting an IContent. */
    this (IWidgetManager mgr, widgetID id, WidgetData data, IContent content) {
        // Get grid size and check data
        // Check sufficient data for type, align-flags, rows, cols, and possibly row/col widths.
        if (data.ints.length != 4) throw new WidgetDataException (this);
        
        rows = data.ints[2];
        cols = data.ints[3];
        // Check: at least one sub-widget, ints length == 3, strings' length is correct:
        if (rows < 1 || cols < 1 || data.ints.length != 4 || data.strings.length != rows * cols)
            throw new WidgetDataException (this);
        
        // Get all sub-widgets
        subWidgets.length = rows*cols;
        foreach (i, ref subWidget; subWidgets) {
            subWidget = mgr.makeWidget (data.strings[i], content);
        }
        
        initWidths = mgr.dimData (id);  // may be null, tested later
        
        super (mgr, id, data);
    }
    
    // Save column/row sizes. Currently always do so.
    bool saveChanges () {
        foreach (widget; subWidgets) // recurse on subwidgets
            widget.saveChanges ();
        
        mgr.setDimData (id, col.width ~ row.width);
        return true;
    }
protected:
}


/*************************************************************************************************
 * Iterates on an ContentList to produce a list of widgets, each of which is created with widgetID
 * data.strings[0]. If an IContent is passed, this is cast to a ContentList, otherwise
 * content.Items is used to get an IContent. It is an error if the content fails to cast to
 * ContentList.
 *************************************************************************************************/
class ContentListWidget : GridWidget
{
    this (IWidgetManager mgr, widgetID id, WidgetData data, IContent content) {
        debug scope (failure)
                logger.warn ("TrialContentLayoutWidget: failure");
        WDCheck (data, 2, 1);
	
	cList = cast(ContentList) content;
	if (cList is null)
	    throw new ContentException;
	
        cols = 1;
        if ((rows = cList.list.length) > 0) {
            subWidgets.length = rows;
            foreach (i, c; cList.list) {
                subWidgets[i] = mgr.makeWidget (data.strings[0], c);
            }
        } else {
            rows = 1;
            subWidgets = [mgr.makeWidget (data.strings[0], new ErrorContent ("<empty list>"))];
        }
        super (mgr, id, data);
    }
    
    bool saveChanges () {
        // Since all sub-widgets have the same id, it only makes sense to call on one
        if (subWidgets is null)
            return false;
        return subWidgets[0].saveChanges;
    }
    
private:
    ContentList cList;
}


/*************************************************************************************************
 * Backend for grid-based (includes column/row) layout widgets.
 *
 * A deriving class must at least do some work in it's constructor (see Ddoc for this() below)
 * and provide an implementation of getCreationData() (unless Widget's version is sufficient).
 *
 * Since a grid with either dimension zero is not useful, there must be at least one sub-widget.
 *
 * The grid has no border but has spacing between widgets.
 *************************************************************************************************/
abstract class GridWidget : AParentWidget
{
    //BEGIN Creation & saving
    /** Partial constructor for a grid layout widget.
     *
     * Deriving classes should check data lengths, and set rows, cols, and the subWidgets array,
     * before calling this super constructor. (If it's necessary to call super(...) first,
     * the call to genCachedConstructionData can be moved to the derived this() methods.)
     * 
     * Derived constructors may also set initWidths to the array of column widths followed by
     * row heights used to initially set the row/column dimensions.
     * 
     * Sub-widgets are finalized here, so no methods should be called on sub-widgets before calling
     * this super. */
    protected this (IWidgetManager mgr, widgetID id, WidgetData data) {
        super (mgr, id, data);
        
        // Create cell aligners with appropriate col/row adjustment function
        if (data.ints[1] & 1)
            col = AlignColumns.getInstance (id, cols);
        else
            col = (new AlignColumns (cols));
        col.addSetCallback (&setColWidth);
        if (data.ints[1] & 2)
            row = AlignColumns.getInstance (id~"R", rows);      // id must be unique to that for cols!
        else
            row = (new AlignColumns (rows));
        row.addSetCallback (&setRowHeight);
    }
    
    /** Prior to finalizing but after sub-widgets are finalized, some information needs to be
     * passed to the AlignColumns. */
    void prefinalize () {
        genCachedConstructionData;  // min widths, sizableness
    }
    
    /** Responsible for calculating the minimal size and initializing some stuff.
     *
     * As such, this must be the first function called after this(). */
    void finalize () {
        if (initWidths.length == cols + rows) {
            col.setWidths (initWidths[0..cols]);
            row.setWidths (initWidths[cols..$]);
        } else {
            col.setWidths;
            row.setWidths;
        }
        initWidths = null;  // free
        
        mw = col.mw;
        mh = row.mw;
        w = col.w;
        h = row.w;
        
        // Tell subwidgets their new sizes. Positions are given by a later call to setPosition.
        foreach (i,widget; subWidgets) {
            // Resizing direction is arbitrarily set to negative:
            widget.setWidth  (col.width[i % cols], -1);
            widget.setHeight (row.width[i / cols], -1);
        }
    }
    //END Creation & saving
    
    //BEGIN Size & position
    bool isWSizable () {
        return col.firstSizable >= 0;
    }
    bool isHSizable () {
        return row.firstSizable >= 0;
    }
    
    void setWidth (wdim nw, int dir) {
        w = col.resizeWidth (nw, dir);
        // Note: setPosition must be called after!
    }
    void setHeight (wdim nh, int dir) {
        h = row.resizeWidth (nh, dir);
        // Note: setPosition must be called after!
    }
    
    void setPosition (wdim x, wdim y) {
        this.x = x;
        this.y = y;
        
        debug assert (col.pos && row.pos, "setPosition: col/row.pos not set (code error)");
        foreach (i,widget; subWidgets)
            widget.setPosition (x + col.pos[i % cols], y + row.pos[i / cols]);
    }
    //END Size & position
    
    
    // Find the relevant widget.
    IChildWidget getWidget (wdim cx, wdim cy) {
        debug scope (failure)
            logger.warn ("getWidget: failure; values: click, pos, width - {}, {}, {} - {}, {}, {}", cx, x, w, cy, y, h);
        debug assert (cx >= x && cx < x + w && cy >= y && cy < y + h, "getWidget: not on widget (code error)");
        
        // Find row/column:
        myDiff i = col.getCell (cx - x);
        myDiff j = row.getCell (cy - y);
        if (i < 0 || j < 0)	// on a space between widgets
            return this;
        
        // On a subwidget; recurse call:
        return subWidgets[i + j*cols].getWidget (cx, cy);
    }
    
    // Resizing columns & rows
    int clickEvent (wdabs cx, wdabs cy, ubyte b, bool state) {
        debug scope (failure)
                logger.warn ("clickEvent: failure");
        if (b == 1 && state == true) {
            /* Note: Because of getWidget, this function is only called if the click is not on a
            * sub-widget, so we know it's on some divisor (so at least one of resizeCol and
            * resizeRow is non-negative). */
            
            // find col/row's resizeD & resizeU
            if (col.findResizeCols (cx - x) && row.findResizeCols (cy - y))
                return 0;		// unable to resize
            
            dragX = cx;
            dragY = cy;
            
            mgr.addClickCallback (&endCallback);
            mgr.addMotionCallback (&resizeCallback);
        }
	return 0;
    }
    
    void draw () {
        super.draw ();
        
        foreach (widget; subWidgets)
            widget.draw ();
    }
    
package:
    /* Calculations which need to be run whenever a new sub-widget structure is set
     * (i.e. to produce cached data calculated from construction data).
     * Also need to be re-run if the renderer changes.
     *
     * rows, cols and subWidgets must be set before calling. Part of the set-up for AlignColumns
     * (col and row). subWidgets need to know their minimal size and resizability. */
    void genCachedConstructionData () {
        // Will only change if renderer changes:
        // NOTE shared AlignColumns get this set by all sharing GridWidgets
        col.spacing = row.spacing = mgr.renderer.layoutSpacing;
        
        // Calculate the minimal column and row sizes:
        // AlignColumns (row, col) takes care of initializing minWidth.
        foreach (i,widget; subWidgets) {
            // Increase dimensions if current minimal size is larger:
            myIt n = i % cols;	// column
            wdim md = widget.minWidth;
            if (col.minWidth[n] < md) col.minWidth[n] = md;
            n = i / cols;		// row
            md = widget.minHeight;
            if (row.minWidth[n] < md) row.minWidth[n] = md;
        }
        
        // Find which cols/rows are resizable:
        // AlignColumns initializes sizable, and sets first and last sizables.
        forCols:
        for (myIt i = 0; i < cols; ++i) {				// for each column
            for (myIt j = 0; j < subWidgets.length; j += cols)	// for each row
                if (!subWidgets[i+j].isWSizable)	// column not resizable
                    continue forCols;			// continue the outer for loop
            
            // column is resizable if we get to here
            col.sizable[i] = true;
        }
        
        forRows:
        for (myIt i = 0; i < subWidgets.length; i += cols) {	// for each row
            for (myIt j = 0; j < cols; ++j)			// for each column
                if (!subWidgets[i+j].isHSizable)
                    continue forRows;
            
            row.sizable[i / cols] = true;
        }
    }
    
private:
    void setColWidth (myIt i, wdim w, int dir) {
        for (myIt j = 0; j < rows; ++j) {
            subWidgets[i + cols*j].setWidth (w, dir);
        }
    }
    void setRowHeight (myIt j, wdim h, int dir) {
        for (myIt i = 0; i < cols; ++i) {
            subWidgets[i + cols*j].setHeight (h, dir);
        }
    }
    
    
    //BEGIN Col/row resizing callback
    void resizeCallback (wdim cx, wdim cy) {
        col.resizeCols (cx - dragX);
        row.resizeCols (cy - dragY);
        
        // NOTE: all adjustments are relative; might be better if they were absolute?
        dragX = cx;
        dragY = cy;
        
        foreach (i,widget; subWidgets)
            widget.setPosition (x + col.pos[i % cols],
                                y + row.pos[i / cols]);
        mgr.requestRedraw;
    }
    bool endCallback (wdabs cx, wdabs cy, ubyte b, bool state) {
        if (b == 1 && state == false) {
            mgr.removeCallbacks (cast(void*) this);
            return true;	// we've handled the up-click
        }
        return false;		// we haven't handled it
    }
    
protected:
    // Data for resizing cols/rows:
    wdim dragX, dragY;	// coords where drag starts
    //END Col/row resizing callback
    
    myIt cols, rows;	// number of cells in grid
    wdim[] initWidths;  // see this / setInitialSize
    
    /* All widgets in the grid, by row. Order:  [ 0 1 ]
     *                                          [ 2 3 ] */
    //IChildWidget[] subWidgets;
    
    AlignColumns col, row;
}


/**************************************************************************************************
 * Alignment device
 * 
 * E.g. can control widths of columns within a grid, and provide sensible resizing, respecting the
 * minimal width required by each cell in a column. Is not restricted to horizontal widths, but to
 * ease descriptions, a horizontal context (column widths) is assumed.
 * 
 * Cells should be of type IChildWidget.
 * 
 * Cells are not directly interacted with, but minimal widths for each column are passed, and
 * callback functions are used to adjust the width of any column.
 *************************************************************************************************/
package class AlignColumns
{
    /** Instance returned will be shared with any other widgets of same widgetID.
     *
     * Also ensures each widget sharing an instance expects the same number of columns. */
    static AlignColumns getInstance (widgetID id, myIt columns) {
        AlignColumns* p = id in instances;
        if (p) {
            if (p.minWidth.length != columns)
                throw new GuiException ("AlignColumns: no. of columns varies between sharing widgets (code error)");
            //logger.trace ("Shared alignment for: "~id);
            return *p;
        } else {
            auto a = new AlignColumns (columns);
            instances[id] = a;
            return a;
        }
    }
    
    /** Create an instance. After creation, the number of columns can only be changed by calling
     * reset.
     * 
     * After creation, minimal widths should be set for all columns (minWidth) and
     * setWidths must be called before other functions are used. */
    this (myIt columns) {
        reset (columns);
    }
    
    /** Reset all column information (only keep set callbacks).
     *
     * Widths should be set after calling, as on creation. */
    void reset (myIt columns) {
        if (columns < 1)
            throw new GuiException("AlignColumns: created with <1 column (code error)");
        minWidth = new wdim[columns];
        sizable = new bool[columns];
        width = null;   // enforce calling setWidths after this
        firstSizable = -1;
        lastSizable = -1;
    }
    
    /** Initialize widths, either from minWidths or from supplied list, checking validity.
     *
     * Also calculates first/lastSizable from sizable, overall minimal width and column positions.
     */
    void setWidths (wdim[] data = null) {
        if (!width) {
            if (data) {
                debug assert (data.length == minWidth.length, "setWidths called with bad data length (code error)");
                width = data.dup;       // data is shared by other widgets with same id so must be .dup'ed
                // And check sizes are valid:
                foreach (i, m; minWidth) {
                    if (!sizable[i] || width[i] < m)    // if width is fixed or less than minimum
                        width[i] = m;
                }
            } else
                width = minWidth.dup;
            
            /* Calculate the minimal width of all columns plus spacing. */
            mw = spacing * cast(wdim)(minWidth.length - 1);
            foreach (imw; minWidth)
                mw += imw;
            
            genPositions;
            
            foreach (i,s; sizable) {
                if (s) {
                    firstSizable = i;
                    goto gotFirst;
                }
            }
            return; // none resizable - don't search for lastSizable
            gotFirst:
            foreach_reverse (i,s; sizable) {
                if (s) {
                    lastSizable = i;
                    return; // done
                }
            }
        }
    }
    
    /** Add a callback to be called to notify changes in a column's width.
    * 
    * All callbacks added are called on a width change so that multiple objects may share a
    * CellAlign object. */
    typeof(this) addSetCallback (void delegate (myIt,wdim,int) setCW) {
        assert (setCW, "CellAlign.this: setCW is null (code error)");
        setWidthCb ~= setCW;
        return this;
    }
    
    /** Get the row/column of relative position l.
     *
     * returns:
     * -i if in space to left of col i, or i if on col i. */
    myDiff getCell (wdim l) {
        debug assert (width, "AlignColumns not initialized when getCell called (code error)");
        myDiff i = minWidth.length - 1;     // starting from right...
        while (l < pos[i]) {                // decrement while left of this column
            debug assert (i > 0, "getCell: l < pos[0] (code error)");
            --i;
        }                                   // now (l >= pos[i])
        if (l >= pos[i] + width[i]) {       // between columns
            debug assert (i+1 < minWidth.length, "getCell: l >= total width (code error)");
            return -i - 1;                  // note: i might be 0 so cannot just return -i
        }
        return i;
    }
    
    /** Adjust total size with direction dir.
     *
     * nw should be at least the minimal width. */
    wdim resizeWidth (wdim nw, int dir) {
        debug assert (width, "AlignColumns not initialized when resizeWidth called (code error)");
        if (nw < mw) {
            debug logger.warn ("Widget dimension set below minimal");
            nw = mw;
        }
        if (nw == w) return w;
        
        wdim diff = nw - w;
        if (firstSizable == -1)
            diff = adjustCellSizes (diff, minWidth.length-1, -1);
        else
            diff = adjustCellSizes (diff, (dir == -1 ? lastSizable : firstSizable), dir);
        
        debug if (nw != w) {
            logger.trace ("resizeWidth on {} to {} failed, new width: {}, diff {}, firstSizable {}, columns {}",cast(void*)this, nw,w, diff, firstSizable, minWidth.length);
            /+ Also print column widths & positions:
            logger.trace ("resizeWidth to {} failed! Column dimensions and positions:",nw);
            foreach (i,w; width)
                logger.trace ("\t{}\t{}", w,pos[i]);+/
        }
        return w;
    }
    
    /** Calculate resizeU/resizeD, and return true if unable to resize.
     *
     * This and resizeCols are for moving dividers between cells. */
    bool findResizeCols (wdim l) {
        resizeU = -getCell (l);             // potential start for upward-resizes
        if (resizeU <= 0)
            return true;        // not on a space between cells
        resizeD = resizeU - 1;              // potential start for downward-resizes
        
        while (!sizable[resizeU]) {         // find first actually resizable column (upwards)
            ++resizeU;
            if (resizeU >= minWidth.length) {       // cannot resize
                resizeU = -1;
                return true;
            }
        }
            
        while (!sizable[resizeD]) {         // find first actually resizable column (downwards)
            --resizeD;
            if (resizeD < 0) {              // cannot resize
                resizeU = -1;       // resizeU is tested to check whether resizes are possible
                return true;
            }
        }
        
        return false;                       // can resize
    }
    /// Resize columns based on findResizeCols
    void resizeCols (wdim diff)
    {
        if (resizeU <= 0) return;
        
        // do shrinking first (in case we hit the minimum)
        if (diff >= 0) {
            diff = -adjustCellSizes (-diff, resizeU, 1);
            adjustCellSizes (diff, resizeD, -1);
        } else {
            diff = -adjustCellSizes (diff, resizeD, -1);
            adjustCellSizes (diff, resizeU, 1);
        }
    }
    
    /* Generate position infomation for each column and set w. */
    private void genPositions () {
        pos.length = minWidth.length;
        
        w = 0;
        foreach (i, cw; width) {
            pos[i] = w;
            w += cw + spacing;
        }
        w -= spacing;
    }
    
    /* Adjust the total size of rows/columns (including spacing) by diff.
    *
    * Params:
    *  diff = amount to increase/decrease the total size
    *  start= index for col/row to start resizing on
    *  incr = direction to resize in (added to index each step). Must be either -1 or +1.
    *
    * Returns:
    *  The amount adjusted. This may be larger than diff, since cellD is clamped by cellDMin.
    *
    * Note: Check variable used for start is valid before calling! If a non-sizable column's
    *  index is passed, this should get increased (if diff > 0) but not decreased.
    */
    private wdim adjustCellSizes (wdim diff, myDiff start, int incr)
    in {
        assert (width.length == minWidth.length, "CellAlign.adjustCellSizes: width is null (code error)");
        // Most likely if passed negative when sizing is disabled:
        assert (start >= 0 && start < minWidth.length, "adjustCellSizes: invalid start");
        debug assert (incr == 1 || incr == -1, "adjustCellSizes: invalid incr");
    } body {
        debug scope(failure) logger.trace ("adjustCellSizes: failure");
        myDiff i = start;
        if (diff > 0) {             // increase size of first resizable cell
            width[i] += diff;
            foreach (dg; setWidthCb)
                dg(i, width[i], incr);
        }
        else if (diff < 0) {        // decrease
            wdim rd = diff;         // running diff
            aCSwhile:
            while (true) {
                width[i] += rd;     // decrease this cell's size (but may be too much)
                rd = width[i] - minWidth[i];
                if (rd >= 0) {      // OK; we're done
                    foreach (dg; setWidthCb)
                        dg(i, width[i], incr);
                    break;          // we hit the mark exactly: diff is correct
                }
                
                // else we decreased it too much!
                width[i] = minWidth[i];
                foreach (dg; setWidthCb)
                    dg(i, width[i], incr);
                // rd is remainder to decrease by
                
                do {
                    i += incr;
                    if (i < 0 || i >= minWidth.length) {    // run out of next cells
                        diff -= rd; // still had rd left to decrease
                        break aCSwhile;     // exception: Array index out of bounds
                    }
                } while (!sizable[i])       // iterate again if row/col isn't resizable
            }
        }
        // else no adjustment needed (diff == 0)
        
        genPositions;
        return diff;
    }
    
    /** Minimal width for each column.
     *
     * Initialized to zero. Each class using this AlignColumns should, for each column, increase
     * this value to the maximum of the minimal widths (in other words, set
     * minWidth[i] = max(minWidth[i], cell.minWidth) for each cell in column i). */
    wdim[]  minWidth;           // minimal widths (set by genCachedConstructionData)
    
    /** For each column i, sizable[i] is true if that column is resizable.
     * 
     * Set along with minWidth before calling setWidths. */
    bool[]  sizable;            // set by genCachedConstructionData
    
    /** Current width, relative position (for each column)
     *
     * Treat as READ ONLY outside this class! */
    wdim[]  width;              // only adjusted within the class
    wdim[]  pos;                /// ditto
protected:
    myDiff  resizeD,            // resizeCols works down from this index (<0 if not resizing)
            resizeU;            // and up from this index
    wdim    spacing;            // used by genPositions (which cannot access the layout class's data)
    wdim    w,mw;               // current & minimal widths
    /* indicies of the first/last resizable column (negative if none are resizable). */
    myDiff  firstSizable = -1, lastSizable = -1;  // set by calcFLSbl
    // Callbacks used to actually adjust a column's width:
    void delegate (myIt,wdim,int) setWidthCb[]; // set width of a column, with resize direction
    
    static HashMap!(widgetID,AlignColumns) instances;
    static this () {
        instances = new HashMap!(widgetID,AlignColumns);
    }
}

// Index types. Note that in some cases they need to hold negative values.
// int is used for resizing direction (although ptrdiff_t would be more appropriate),
// since the value must always be -1 or +1.
alias size_t myIt;
alias ptrdiff_t myDiff;