view mde/gui/widget/layout.d @ 78:79a1809421aa

Widget data saving reimplemented for the new data system. (Now grid layout widgets remember their row & column sizes.)
author Diggory Hardy <diggory.hardy@gmail.com>
date Tue, 29 Jul 2008 18:14:53 +0100
parents 3dfd934100f7
children 56c0ddd90193
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.widget.TextWidget;
import mde.gui.content.options;

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:
     * [widgetID, r, c, 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. */
    this (IWidgetManager mgr, WidgetData data) {
        // Get grid size and check data
        // Check sufficient data for rows, cols, and possibly row/col widths.
        if (data.ints.length < 3) throw new WidgetDataException;
        
        rows = data.ints[1];
        cols = data.ints[2];
        // Check: at least one sub-widget, ints length == 3 or also contains row & col widths,
        // strings' length is correct:
        if (rows < 1 || cols < 1 ||
            (data.ints.length != 3 && data.ints.length != 3 + rows + cols) ||
            data.strings.length != rows * cols)
            throw new WidgetDataException;
        this.data = data;
        
        // Get all sub-widgets
        subWidgets.length = rows*cols;
        foreach (i, ref subWidget; subWidgets) {
            subWidget = mgr.makeWidget (data.strings[i]);
        }
        
        super (mgr, data);
        
        if (data.ints.length == 3 + rows + cols) {
            col.setCheck (cast(wdim[]) data.ints[3..cols+3]);
            row.setCheck (cast(wdim[]) data.ints[cols+3..$]);
        } else {
            col.dupMin;
            row.dupMin;
        }
        adjustCache;
    }
    
    // Save column/row sizes. Currently always do so.
    bool saveChanges (widgetID id) {
        with (data) {
            foreach (i, widget; subWidgets) // recurse on subwidgets
                widget.saveChanges (strings[i]);
            
            ints = ints[0..3] ~ cast(int[])col.width ~ cast(int[])row.width;
        }
        mgr.setData (id, data);
        return true;
    }
protected:
    WidgetData data;
}


/*************************************************************************************************
 * Trial layout of sub-widgets of one type only.
 *************************************************************************************************/
class TrialContentLayoutWidget : GridWidget
{
    this (IWidgetManager mgr, WidgetData data) {
        debug scope (failure)
                logger.warn ("TrialContentLayoutWidget: failure");
        WDCheck (data, 2);
        
        OptionList optsList = OptionList.trial();
        rows = optsList.list.length;
        cols = 1;
        
        // Get all sub-widgets
        subWidgets.length = rows*cols;
        WidgetData COWData;
        COWData.ints = [0, data.ints[1]];
        foreach (i, c; optsList.list) {
            subWidgets[i] = new ContentOptionWidget (mgr, COWData, c);
        }
        super (mgr, data);
        
        // Set col/row widths to minimals.
        col.dupMin;
        row.dupMin;
        adjustCache;
    }
    
private:
    OptionList optsList;
}


/*************************************************************************************************
 * 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 : Widget
{
    //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 should call either dupMin or setCheck on col and row, and then call
     * adjustCache, after calling this. */
    protected this (IWidgetManager mgr, WidgetData data) {
        super (mgr, data);
        
        // Needn't be set before genCachedConstructionData is called:
        col.setColWidth = &setColWidth;
        row.setColWidth = &setRowHeight;
        
        // Calculate cached construction data
        genCachedConstructionData;
    }
    
    /** Generates cached mutable data.
     *
     * Should be called by adjust() after setting col and row widths (currently via dupMin or
     * setCheck). */
    void adjustCache () {
        // Generate cached mutable data
        // Calculate column and row locations:
        w = col.genPositions;
        h = row.genPositions;
        
        // 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) {
        if (nw == w) return;
        
        w += col.adjustCellSizes (nw - w, (dir == -1 ? col.lastSizable : col.firstSizable), dir);
        
        // Note: setPosition must be called after!
    }
    void setHeight (wdim nh, int dir) {
        if (nh == h) return;
        
        h += row.adjustCellSizes (nh - h, (dir == -1 ? row.lastSizable : row.firstSizable), dir);
        
        // Note: setPosition must be called after!
    }
    
    void setPosition (wdim x, wdim y) {
        this.x = x;
        this.y = y;
        
        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");
        // 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
    void 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.findResize (cx - x) && row.findResize (cy - y))
                return;		// unable to resize
            
            dragX = cx;
            dragY = cy;
            
            mgr.addClickCallback (&endCallback);
            mgr.addMotionCallback (&resizeCallback);
        }
    }
    
    void draw () {
        super.draw ();
        
        foreach (widget; subWidgets)
            widget.draw ();
    }
    
private:
    //BEGIN Cache calculation functions
    /* 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. */
    void genCachedConstructionData () {
        // Will only change if renderer changes:
        col.spacing = row.spacing = mgr.renderer.layoutSpacing;
        
        // Calculate the minimal column and row sizes:
        // set length, making sure the arrays are initialised to zero:
        col.minWidth = new wdim[cols];
        row.minWidth = new wdim[rows];
        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;
        }
        
        
        // Calculate the overall minimal size, starting with the spacing:
        mh = mgr.renderer.layoutSpacing;	// use mh temporarily
        mw = mh * cast(wdim)(cols - 1);
        mh *= cast(wdim)(rows - 1);
        
        foreach (x; col.minWidth)		// add the column/row's dimensions
            mw += x;
        foreach (x; row.minWidth)
            mh += x;
        
        
        // Find which cols/rows are resizable:
        // reset:
        col.sizable = new bool[cols];
        row.sizable = new bool[rows];
        col.firstSizable = row.firstSizable = -1;
        
        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;
            if (col.firstSizable < 0)
                col.firstSizable = i;
            col.lastSizable = i;
        }
        
        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.lastSizable = i / cols;
            row.sizable[row.lastSizable] = true;
            if (row.firstSizable < 0)
                row.firstSizable = row.lastSizable;
        }
    }
    //END Cache calculation functions
    
    
    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.resize (cx - dragX);
        row.resize (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
    
    /* All widgets in the grid, by row. Order:  [ 0 1 ]
     *                                          [ 2 3 ] */
    IChildWidget[] subWidgets;
    
    /* Widths, positions, etc., either of columns or of rows
     *
     * The purpose of this struct is mostly to unify functionality which must work the same on both
     * horizontal and vertical cell placement.
     *
     * Most notation corresponds to horizontal layout (columns), simply for easy of naming. */
    struct CellDimensions {
        wdim[] pos,	// relative position (cumulative width[i-1] plus spacing)
              width,	// current widths
              minWidth;	// minimal widths (set by genCachedConstructionData)
        bool[] sizable;	// true if col is resizable (set by genCachedConstructionData)
        myDiff firstSizable,	// first col which is resizable, negative if none
               lastSizable;	// as above, but last (set by genCachedConstructionData)
        myDiff resizeD,	// resize 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)
        /* This is a delegate to a enclosing class's function, since:
         * a different implementation is needed for cols or rows
         * we're unable to access enclosing class members directly */
        void delegate (myIt,wdim,int) setColWidth;	// set width of a column, with resize direction
        
        void dupMin () {
            width = minWidth.dup;
        }
        void setCheck (wdim[] data) {
            // Set to provided data:
            width = data;
            // And check sizes are valid:
            foreach (i, m; minWidth)
                // if fixed width or width is less than minimum:
                if (!sizable[i] || width[i] < m)
                    width[i] = m;
        }
        
        // Generate position infomation and return total width (i.e. widget width/height)
        wdim genPositions () {
            pos.length = minWidth.length;
            
            wdim x = 0;
            foreach (i, w; width) {
                pos[i] = x;
                x += w + spacing;
            }
            return x - spacing;
        }
        
        // Get the row/column a click occured in
        // Returns -i if in space to left of col i, or i if on col i
        myDiff getCell (wdim l) {
            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
                return -i - 1;			// note: i might be 0 so cannot just return -i
            return i;
        }
        
        // Calculate resizeU/resizeD, and return true if unable to resize.
        bool findResize (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
        }
        
        /* 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.
        */
        wdim adjustCellSizes (wdim diff, myDiff start, int incr)
        in {
            // Could occur if constructor doesn't call dupMin/setCheck (code error):
            assert (width !is null, "adjustCellSizes: width is null");
            // Most likely if passed negative when sizing is disabled:
            assert (start >= 0 && start < minWidth.length, "adjustCellSizes: invalid start");
            assert (incr == 1 || incr == -1, "adjustCellSizes: invalid incr");
            assert (setColWidth !is null, "adjustCellSizes: setColWidth is null");
        } body {
            debug scope(failure)
                    logger.trace ("adjustCellSizes: failure");
            myDiff i = start;
            if (diff > 0) {		// increase size of first resizable cell
                width[i] += diff;
                setColWidth (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
                        setColWidth (i, width[i], incr);	// set new width
                        break;		// we hit the mark exactly: diff is correct
                    }
                    
                    // else we decreased it too much!
                    width[i] = minWidth[i];
                    setColWidth (i, width[i], incr);
                    // rd is remainder to decrease by
                    
                    
                    bool it = true;	// iterate (force first time)
                    while (it) {
                        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
                        }
                        it = !sizable[i];	// iterate again if row/col isn't resizable
                    }
                }
            }
            // else no adjustment needed (diff == 0)
            
            genPositions;
            return diff;
        }
        
        void resize (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);
            }
        }
    }
    CellDimensions col, row;
    
    // 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 and int is smaller on X86_64.
    alias size_t myIt;
    alias ptrdiff_t myDiff;
}