view mde/gui/widget/layout.d @ 105:08651e8a8c51

Quit button, big changes to content system. Moved mde.gui.content to mde.content to reflect it's not only used by the gui. Split Content module into Content and AStringContent. New AContent and EventContent class. Callbacks are now generic and implemented in AContent. Renamed TextContent to StringContent and ValueContent to AStringContent.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 29 Nov 2008 12:36:39 +0000
parents 42e241e7be3e
children 2a1428ec5344
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.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;