view mde/gui/widget/Floating.d @ 91:4d5d53e4f881

Shared alignment for dynamic content lists - finally implemented! Lots of smaller changes too. Some debugging improvements. When multiple .mtt files are read for merging, files with invalid headers are ignored and no error is thrown so long as at least one file os valid.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 16 Oct 2008 17:43:48 +0100
parents b525ff28774b
children 085f2ca31914
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/>. */

/** The Window class. Becoming a widget. */
module mde.gui.widget.Floating;

import mde.gui.widget.Widget;
import mde.gui.exception;
/+import mde.gui.widget.createWidget;

import mde.gui.IGui;
import mde.gui.renderer.createRenderer;

import mt = mde.mergetag.DataSet;
import mde.mergetag.parse.parseTo : parseTo;
import mde.mergetag.parse.parseFrom : parseFrom;
+/
import tango.util.log.Log : Log, Logger;

private Logger logger;
static this () {
    logger = Log.getLogger ("mde.gui.widget.Floating");
}
//FIXME - documentation

/** GUI Window class
 *
 * A window class instance does two things: (1) specify a region of the screen upon which the window
 * and its associated widgets are drawn, and (2) load, save, and generally manage all its widgets.
 *
 * Let the window load a table of widget data, of type int[][widgetID]. Each widget will, when
 * created, be given its int[] of data, which this() must confirm is valid (or throw).
 */
/** An area to contain floating widgets.
 *
 * The position of each sub-widget is set from data, but not the size.
 * Rationale: parents' need to set subwidgets' positions when its position is set, so it needs to
 * know their positions. Size setting is still under work FIXME.
 *
 * Data: Each string item is interpreted as a subwidget widgetID.
 * Ints supplied may consist of just the widget type or
 * additionally an (x,y) coordinate for each subwidget (all x coords first, then all y coords).
 */
class FloatingAreaWidget : SizableWidget
{
    this (IWidgetManager mgr, widgetID id, WidgetData data) {
        subWidgets.length = data.strings.length;
        foreach (i,s; data.strings)
            subWidgets[i] = mgr.makeWidget (s);
        sWCoords.length = subWidgets.length;
        
        if (data.ints.length != 1) {
            if (data.ints.length != 2*subWidgets.length + 1) {
                throw new WidgetDataException (this);
            }
            foreach (i, ref c; sWCoords) {
                c.x = cast(wdim) data.ints[i + 1];
                c.y = cast(wdim) data.ints[i + 1 + sWCoords.length];
            }
        }
        
        super (mgr, id, data);
        
        foreach (w; subWidgets) {
            //FIXME: set default size
            w.setWidth  (w.minWidth, -1);
            w.setHeight (w.minHeight, -1);
        }
    }
    
    void setPosition (wdim x, wdim y) {
        super.setPosition (x,y);
        
        foreach (i,c; sWCoords)
            subWidgets[i].setPosition (x+c.x, y+c.y);
    }
    
    void draw () {
        super.draw;
        
        mgr.renderer.restrict (x,y, w,h);
        
        foreach (w; subWidgets)
            w.draw;
    }
    
protected:
    IChildWidget[] subWidgets;  // all subwidgets, framed or not
    wdimPair[] sWCoords;        // coords for subwidgets, relative to this widget
    
    /+
    /** Call after loading is finished to setup the window and confirm that it's valid.
     *
     * Throws: WindowLoadException (or possibly other exceptions). Do not use the instance if an
     * exception occurs! */
    void finalise (IGui gui)
    in {
        assert (gui !is null, "Window.finalise ("~name~"): gui is null");
    } body {
        // Check data was loaded:
        if (widgetData is null)
            throw new WindowLoadException ("No widget creation data");
        
        // Save gui and create the renderer:
        gui_ = gui;
        rend = createRenderer (gui.rendererName);
        
        // Create the primary widget (and indirectly all sub-widgets), throwing on error:
        // Note: GridLayoutWidget's this relies on rend.layoutSpacing.
        widget = makeWidget (0);        // primary widget always has ID 0.
        // This data is no longer needed by Window, although its sub-arrays may still be used, so
        // let the GC collect what it can:
        widgetData = null;
        widgetStrings = null;
        
        // get border sizes:
        border = rend.setSizable (isWSizable, isHSizable);  // depends on widget
        
        // Note: this should return an empty array, but we shouldn't make a fuss if it's not empty:
        if ((widget.adjust (mutableData)).length != 0)	// adjust/set size, etc., depends on rend
            logger.warn ("Local widget position data is invalid!");
        mutableData = null;             // no longer needed
        
        widget.getCurrentSize (w,h);    // and get this size
        w += border.l + border.r;       // Adjust for border
        h += border.t + border.b;
        
        widgetX = x + border.l;         // widget position
        widgetY = y + border.t;         // must be updated if the window is moved
        widget.setPosition (widgetX, widgetY);
        
        // Calculate mw/mh and xw/yh (cached data):
        mw = widget.minWidth  + border.l + border.r;
        mh = widget.minHeight + border.t + border.b;
        
        xw = x+w;
        yh = y+h;
    }
    //END Methods for GUI
    
    //BEGIN IWindow methods
    /** Get/create a widget by ID.
     *
     * Should $(I only) be called internally and by sub-widgets! */
    IWidget makeWidget (widgetID i)
    in {
        // widgetData is normally left to be garbage collected after widgets have been created:
        assert (widgetData !is null, "Window.makeWidget ("~name~"): widgetData is null");
    } body {
        /* Each widget returned should be a unique object; if multiple widgets are requested with
        * the same ID, a new widget is created each time. */
        
        int[]* d = i in widgetData;
        if (d is null)
            throw new WindowLoadException ("Window.makeWidget ("~name~"): Widget not found");
        
        // Throws WidgetDataException (a WindowLoadException) if bad data:
        return createWidget (this, *d);
    }
    
    char[] getWidgetString (int i)
    in {
        // widgetStrings is freed at same time as widgetData
        // but widgetData is guaranteed to be read
        assert (widgetData !is null, "getWidgetString called after widget creation finished");
    } body {
        char[]* p;
        if (widgetStrings is null ||
             (p = i in widgetStrings) is null )
            throw new WindowLoadException ("Needed widgetStrings not set for Window");
        
        return *p;
    }
    
    /** Add this widget's data to that to be saved, returning it's widgetID. */
    widgetID addCreationData (IWidget widget)
    {
        widgetID i;
        if (widgetData is null)
            i = 0;
        else
            i = widgetData.keys[$-1] + 1;
        
        /+ Doesn't this have no effect except when getCreationData throws, in which case the data
         + isn't used anyway? I'm sure it was added for a reason... FIXME and below
        widgetData[i] = null;   // Make sure the same ID doesn't get used by a recursive call
        +/
        widgetData[i] = widget.getCreationData;
        
        return i;
    }
    
    int addWidgetString (char[] str)
    {
        int i;
        if (widgetStrings is null)
            i = 0;
        else
            i = widgetStrings.keys[$-1] + 1;
        
        /+ See above. FIXME
        widgetStrings[i] = null;   // Make sure the same ID doesn't get used by a recursive call
        +/
        widgetStrings[i] = str;
        
        return i;
    }
    
    IGui gui () {
        return gui_;
    }
    
    void requestRedraw () {
        gui_.requestRedraw;
    }
    
    IRenderer renderer ()
    in {
        assert (rend !is null, "Window.renderer: rend is null");
    } body {
        return rend;
    }
    //END IWindow methods
    
    //BEGIN IWidget methods
    //FIXME: how many of these methods are actually needed/used?
    int[] adjust (int[]) {          // simply not relevant (never used)
        return [];
    }
    int[] getCreationData () {      // simply not relevant (never used)
        return [];
    }
    int[] getMutableData () {       // simply not relevant (never used)
        return [];
    }
    
    bool isWSizable () {
        return widget.isWSizable;
    }
    bool isHSizable () {
        return widget.isHSizable;
    }
    
    wdim minWidth () {
        return mw;
    }
    wdim minHeight () {
        return mh;
    }
    void getCurrentSize (out wdim cw, out wdim ch) {
        cw = w;
        ch = h;
    }
    
    void setWidth (wdim nw, int dir) {
        if (nw < mw) w = mw;	// clamp
        else w = nw;
        xw = x + w;
        widget.setWidth (w - border.l - border.r, dir);
    }
    void setHeight (wdim nh, int dir) {
        if (nh < mh) h = mh;	// clamp
        else h = nh;
        yh = y + h;
        widget.setHeight (h - border.t - border.b, dir);
    }
    
    void setPosition (wdim nx, wdim ny) {
        x = nx;
        y = ny;
        
        xw = x+w;
        yh = y+h;
        
        widgetX = x + border.l;
        widgetY = y + border.t;
        
        widget.setPosition (widgetX, widgetY);
        
        gui_.requestRedraw ();  // necessary whenever the window is moved; setPosition is called after resizes and moves
    }
    
    IWidget getWidget (wdim cx, wdim cy) {
        if (cx < x || cx >= xw || cy < y || cy >= yh)   // not over window
            return null;
        if (cx >= widgetX && cx < xw-border.r && cy >= widgetY && cy < yh-border.b)
                                                        // over the widget
            return widget.getWidget (cx, cy);
        else                                            // over the window border
            return this;
    }
    void clickEvent (wdabs cx, wdabs cy, ubyte b, bool state) {
        if (b == 1 && state == true) {
            resizeType = rend.getResizeType (cx-x, cy-y, w,h);
            
            if (resizeType != RESIZE_TYPE.NONE) {    // Some type of resize
                // Set x/yDrag (unfortunately these need to be different for each edge)
                if (resizeType & RESIZE_TYPE.L)
                    xDrag = w + cx;
                else if (resizeType & RESIZE_TYPE.R)
                    xDrag = w - cx;
                
                if (resizeType & RESIZE_TYPE.T)
                    yDrag = h + cy;
                else if (resizeType & RESIZE_TYPE.B)
                    yDrag = h - cy;
                
                // Add the callbacks (they use resizeType which has already been set)
                gui_.addClickCallback (&endCallback);
                gui_.addMotionCallback (&resizeCallback);
            } else {                             // window is being moved
                xDrag = cx - x;
                yDrag = cy - y;
                
                gui_.addClickCallback (&endCallback);
                gui_.addMotionCallback (&moveCallback);
            }
        }
    }
    
    void draw () {
        // background
        rend.drawWindow (x,y, w,h);
        
        // Tell the widget to draw itself:
        widget.draw();
    }
    //END IWidget methods
    
private:
    alias IRenderer.BorderDimensions BorderDimensions;
    alias IRenderer.RESIZE_TYPE RESIZE_TYPE;
    
    //BEGIN Window moving and resizing
    void moveCallback (wdabs cx, wdabs cy) {
        setPosition (cx-xDrag, cy-yDrag);
    }
    void resizeCallback (wdabs cx, wdabs cy) {
        debug scope(failure)
                logger.trace ("resizeCallback: failure");
        
        // This function is only called if some resize is going to happen.
        // x,y are used as parameters to setPosition as well as being affected by it; somewhat
        // pointless but fairly effective.
        
        if (resizeType & RESIZE_TYPE.L) {
            wdim xSize = xDrag - cx;
            if (xSize < mw) xSize = mw;	// clamp
            x += w - xSize;
            setWidth (xSize, 1);
        }
        else if (resizeType & RESIZE_TYPE.R) {
            setWidth (xDrag + cx, -1);
        }
        if (resizeType & RESIZE_TYPE.T) {
            wdim ySize = yDrag - cy;
            if (ySize < mh) ySize = mh;
            y += h - ySize;
            setHeight (ySize, 1);
        }
        else if (resizeType & RESIZE_TYPE.B) {
            setHeight (yDrag + cy, -1);
        }
        
        // Moves lower (x,y) coordinate if necessary and repositions any sub-widgets moved by the
        // resizing:
        setPosition (x, y);
    }
    bool endCallback (wdabs cx, wdabs cy, ubyte b, bool state) {
        if (b == 1 && state == false) {
            // The mouse shouldn't have moved since the motion callback
            // was last called, so there's nothing else to do now.
            gui_.removeCallbacks (cast(void*) this);
            
            return true;    // we've handled the up-click
        }
        return false;       // we haven't handled it
    }
    wdim xDrag, yDrag;              // where a drag starts relative to x and y
    IRenderer.RESIZE_TYPE resizeType;   // Type of current resize
    //END Window moving and resizing
    
    // Load/save data:
    public char[] name;             // The window's name (id from config file)
    //bool edited = false;            // True if any widgets have been edited (excluding scaling)
    
    IGui gui_;                      // The gui managing this window
    IRenderer rend;                 // The window's renderer
    
    IWidget widget;                 // The primary widget in this window.
    
    wdim x = -1, y = -1;            // Window position
    wdsize w,h;                     // Window size (calculated from Widgets)
    wdim xw, yh;                    // x+w, y+h (frequent use by clickEvent)
    wdim widgetX, widgetY;          // Widget position (= window position plus BORDER_WIDTH)
    wdim mw = -1, mh = -1;          // minimal size (negative means requires calculation)
    
    BorderDimensions border;        // Total border size (move plus resize)
    +/
}