view mde/gui/widget/Window.d @ 45:0fd51d2c6c8a

Several changes to resising windows and layout widgets. This commit still has some bugs. Moved the implementable widgets from mde.gui.widget.Widget to miscWidgets, leaving base widgets in Widget. Rewrote some of GridLayoutWidget's implementation. Made many operations general to work for either columns or rows. Some optimisations were intended but ended up being removed due to problems. Allowed layout's to resize from either direction (only with window resizes). committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 22 May 2008 11:34:09 +0100
parents b3a6ca4516b4
children 03fa79a48c48
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. Hopefully eventually this will become a widget to make things a bit more
* generic. */
module mde.gui.widget.Window;

import mde.gui.widget.Ifaces;
import mde.gui.widget.createWidget;

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

import mt = mde.mergetag.DataSet;
import tango.scrapple.text.convert.parseTo : parseTo;
import tango.scrapple.text.convert.parseFrom : parseFrom;

import tango.util.log.Log : Log, Logger;

private Logger logger;
static this () {
    logger = Log.getLogger ("mde.gui.widget.Window");
}

/** 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).
 */
class Window : mt.IDataSection, IWindow
{
    //BEGIN Methods for GUI
    this (char[] id) {
        name = id;
    }
    
    /** 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.
        widgetData = null;  // data is no longer needed: allow GC to collect (cannot safely delete)
        
        // 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:
        widget.adjust (mutableData);    // adjust/set size, etc., depends on rend
        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);
        
        xw = x+w;
        yh = y+h;
    }
    //BEGIN Mergetag code
    void addTag (char[] tp, mt.ID id, char[] dt) {
        // Priority is HIGH_LOW, so don't overwrite data which has already been loaded.
        if (tp == INTAA) {
            if (id == WDGD && widgetData == null) {
                widgetData = cast(int[][widgetID]) parseTo!(int[][int]) (dt);
            }
        } else if (tp == INTA) {
            if (id == MD && mutableData == null) {
                mutableData = parseTo!(int[]) (dt);
            }
        } else if (tp == INT) {
            if (id == X && x == -1) {
                x = parseTo!(int) (dt);
            } else if (id == Y && y == -1) {
                y = parseTo!(int) (dt);
            }
        }
    }
    void writeAll (ItemDelg dlg)
    in {
        assert (widgetData is null, "Window.writeAll: widgetData !is null");
    } body {
        /+ NOTE: currently editing is impossible...
        if (edited) {   // only save the widget creation data if it's been adjusted:
            addCreationData (widget);   // generate widget save data
            dlg (INTAA, WDGD, parseFrom!(int[][int]) (widgetData));
        }+/
        // Save mutable data:
        dlg (INTA, MD, parseFrom!(int[]) (widget.getMutableData));
        // Save the window position:
        dlg (INT, X, parseFrom!(int) (x));
        dlg (INT, Y, parseFrom!(int) (y));
    }
    private static const {
        auto INTAA = "int[][int]";
        auto INTA = "int[]";
        auto INT = "int";
        auto WDGD = "widgetData";
        auto MD = "mutableData";
        auto X = "x";
        auto Y = "y";
    }
    //END Mergetag code
    //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);
    }
    
    /** 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;
        
        widgetData[i] = null;   // Make sure the same ID doesn't get used by a recursive call
        widgetData[i] = widget.getCreationData;
        
        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;
    }
    
    void getMinimalSize (out int wM, out int hM) {
        if (mh < 0) {       // calculate if necessary
            widget.getMinimalSize (mw, mh);
            mw += border.l + border.r;
            mh += border.t + border.b;
        }
        wM = mw;
        hM = mh;
    }
    void getCurrentSize (out int cw, out int ch) {
        cw = w;
        ch = h;
    }
    
    void setSize (int nw, int nh, bool wB, bool hB) {
        getMinimalSize (w,h);
        if (nw > w) w = nw;     // expand if new size is larger, but don't go smaller
        if (nh > h) h = nh;
        
        xw = x+w;
        yh = y+h;
        
        widget.setSize (w - border.l - border.r, h - border.t - border.b, wB, hB);
        
        gui_.requestRedraw ();  // obviously necessary whenever the window's size is changed
    }
    
    void setPosition (int nx, int 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 ();  // obviously necessary whenever the window is moved
    }
    
    IWidget getWidget (int cx, int 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 (ushort cx, ushort 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 (ushort cx, ushort cy) {
        setPosition (cx-xDrag, cy-yDrag);
    }
    void resizeCallback (ushort cx, ushort cy) {
        debug scope(failure)
                logger.trace ("resizeCallback: failure");
        
        // This function is only called if some resize is going to happen.
        // To improve efficiency, store parameters to resize to.
        int xSize = w, ySize = h;	// new size
        int xDiff, yDiff;		// difference to new position
        bool xHigh, yHigh;		// resize from positive side
        
        if (resizeType & RESIZE_TYPE.L) {
            getMinimalSize (xDiff, xSize);	// (only want xDiff, temporarily used as mw)
            xSize = xDrag - cx;
            if (xSize < xDiff) xSize = xDiff;	// clamp
            xDiff = w - xSize;			// now used as amount to move
        }
        else if (resizeType & RESIZE_TYPE.R) {
            xSize = xDrag + cx;
            xHigh = true;
        }
        if (resizeType & RESIZE_TYPE.T) {
            getMinimalSize (ySize, yDiff);
            ySize = yDrag - cy;
            if (ySize < yDiff) ySize = yDiff;
            yDiff = h - ySize;
        }
        else if (resizeType & RESIZE_TYPE.B) {
            ySize = yDrag + cy;
            yHigh = true;
        }
        
        setSize (xSize, ySize, xHigh, yHigh);
        if (xDiff != 0 || yDiff != 0)
            setPosition (x + xDiff, y + yDiff);
    }
    bool endCallback (ushort cx, ushort 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
    }
    int 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)
    
    // Data used for saving and loading (null in between):
    int[][widgetID] widgetData = null;// Data for all widgets under this window
    int[] mutableData = null;       // Widget's mutable data (adjusted sizes, etc.)
    
    IGui gui_;                      // The gui managing this window
    IRenderer rend;                 // The window's renderer
    
    IWidget widget;                 // The primary widget in this window.
    
    int x = -1, y = -1;             // Window position
    int w,h;                        // Window size (calculated from Widgets)
    int xw, yh;                     // x+w, y+h (frequent use by clickEvent)
    int widgetX, widgetY;           // Widget position (= window position plus BORDER_WIDTH)
    int mw = -1, mh = -1;           // minimal size (negative means requires calculation)
    
    BorderDimensions border;        // Total border size (move plus resize)
}