view mde/gui/widget/Window.d @ 40:b28d7adc786b

Made GUI more robust to mutable data changes and improved much of GridLayoutWidget's code. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 08 May 2008 16:05:51 +0100
parents 5132301e9ed7
children b3a6ca4516b4
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 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");
        
        gui_ = gui;
        rend = gui.renderer;
        
        // Get border sizes
        border = rend.getBorder (BORDER_TYPES.WINDOW_TOTAL);
        resize = rend.getBorder (BORDER_TYPES.WINDOW_RESIZE);
        
        // Create the primary widget (and indirectly all sub-widgets), throwing on error:
        widget = makeWidget (0);    // primary widget always has ID 0.
        widgetData = null;  // data is no longer needed: allow GC to collect (cannot safely delete)
        
        // Note: this should return an empty array, but nothing much should happen if it's not empty:
        widget.adjust (mutableData);    // adjust/set size, etc.
        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_;
    }
    
    IRenderer renderer () {
        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) {
        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);
        
        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) {
            if (cx < x + resize.l || cx >= xw - resize.r ||
                cy < y + resize.t || cy >= yh - resize.b) { // window is being resized
                /* check for resizes (different to above; use whole border giving larger area for
                 * diagonal resizes). */
                resizeType = RESIZE.NONE;
                
                if (isWSizable) {
                    if (cx < x + border.l) {
                        xDrag = w + cx;
                        resizeType = RESIZE.L;
                    }
                    else if (cx >= xw - border.r) {
                        xDrag = w - cx;
                        resizeType = RESIZE.R;
                    }
                }
                if (isHSizable) {
                    if (cy < y + border.t) {
                        yDrag = h + cy;
                        resizeType |= RESIZE.T;
                    }
                    else if (cy >= yh - border.b) {
                        yDrag = h - cy;
                        resizeType |= RESIZE.B;
                    }
                }
                
                if (resizeType != RESIZE.NONE) {    // only if some valid size is being done
                    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.BORDER_TYPES        BORDER_TYPES;
    
    //BEGIN Window moving and resizing
    void moveCallback (ushort cx, ushort cy) {
        setPosition (cx-xDrag, cy-yDrag);
    }
    void resizeCallback (ushort cx, ushort cy) {
        if (resizeType & RESIZE.L) {
            int mw, nw;
            getMinimalSize (mw, nw);    // (only want mw)
            nw = xDrag - cx;
            if (nw < mw) nw = mw;       // clamp
            mw = x + w - nw;            // reuse
            setSize (nw, h);
            setPosition (mw, y);
        }
        else if (resizeType & RESIZE.R) {
            setSize (xDrag + cx, h);
            setPosition (x, y);
        }
        if (resizeType & RESIZE.T) {
            int mh, nh;
            getMinimalSize (nh, mh);
            nh = yDrag - cy;
            if (nh < mh) nh = mh;
            mh = y + h - nh;
            setSize (w, nh);
            setPosition (x, mh);
        }
        else if (resizeType & RESIZE.B) {
            setSize (w, yDrag + cy);
            setPosition (x, y);
        }
    }
    void 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);
        }
    }
    int xDrag, yDrag;               // where a drag starts relative to x and y
    enum RESIZE : ubyte {
        NONE = 0x0, L = 0x1, R = 0x2, T = 0x4, B = 0x8
    }
    RESIZE 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
    
    BorderDimensions border;        // Total border size (move plus resize)
    BorderDimensions resize;        // The outer resize part of the border
}