view mde/gui/WidgetManager.d @ 125:3e648bc53bde

Added a simple switch/tab widget (depends on existing EnumContent).
author Diggory Hardy <diggory.hardy@gmail.com>
date Tue, 06 Jan 2009 16:54:04 +0000
parents d3b2cefd46c9
children c9843fbaac88
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 gui manager class base.
 *
 * This contains most of the code required by a window manager, but does not interact with a screen
 * or get user input. Rendering is handled separately by the renderer anyway.
 *************************************************************************************************/
module mde.gui.WidgetManager;

import mde.gui.WidgetDataSet;
import mde.gui.widget.Ifaces;
import mde.gui.exception;
import mde.content.Content;

import imde = mde.imde;
import mde.file.mergetag.Reader;
import mde.file.mergetag.Writer;
import mde.setup.paths;

// Widgets to create:
import mde.gui.widget.layout;
import mde.gui.widget.miscWidgets;
import mde.gui.widget.TextWidget;
import mde.gui.widget.miscContent;
import mde.gui.widget.Floating;
import mde.gui.widget.PopupMenu;

import tango.core.sync.Mutex;
import tango.util.log.Log : Log, Logger;
import tango.util.container.SortedMap;

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

/*************************************************************************************************
 * Contains the code for loading and saving an entire gui (more than one may exist), but not the
 * code for drawing it or handling user input.
 * 
 * This abstract class exists solely for separating out some of the functionality.
 *************************************************************************************************/
abstract scope class AWidgetManager : IWidgetManager
{
    /** Construct a new widget loader.
    * 
    * params:
    *  fileName = Name of file specifying the gui, excluding path and extension.
    */
    protected this (char[] file) {
        mutex = new Mutex;  // Used on functions intended to be called from outside the gui package.
        fileName = file;
    }
    
    /* Load the widgets' data from the file specified to the CTOR.
    * 
    * params:
    *  allDesigns = Load all sections
    */
    private void loadData (bool allDesigns = false) {
        if (allLoaded || (defaultDesign !is null && allDesigns == false))
            return; // test if already loaded
            
            // Set up a reader
            scope IReader reader;
        try {
            reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_LOW, null, true);
            
            // Read from the HEADER:
            // Get the renderer
            char[]* p = "Renderer" in reader.dataset.header._charA;
            if (p is null || *p is null) {
                logger.warn ("No renderer specified: using \"Simple\"");
                rendName = "Simple";
            }
            else
                rendName = *p;
            
            // Get which section to use
            p = "Design" in reader.dataset.header._charA;
            if (p is null || *p is null) {
                logger.warn ("No gui design specified: trying \"Default\"");
                defaultDesign = "Default";
            }
            else
                defaultDesign = *p;
            
            // Read the body:
            // Load the chosen design
            reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) {
                WidgetDataSet* p = id in data;
                if (p is null) {
                    data[id] = new WidgetDataSet;
                    return *(id in data);
                }
                return *p;
            };
            
            if (allDesigns) {
                reader.read;
                allLoaded = true;
            } else
                reader.read([defaultDesign]);
        } catch (NoFileException) {
            logger.error ("Unable to load GUI: no config file: "~fileName);
            // just return: not a fatal error (so long as the game can run without a GUI!)
        } catch (Exception e) {
            logger.error ("Unable to load GUI: errors parsing config file ("~confDir.getFileName(fileName,PRIORITY.HIGH_LOW)~"):");
            logger.error (e.msg);
            throw new GuiException ("Failure parsing config file");
        }
    }
    
    /** Load the gui from some design.
    * 
    * If a design was previously loaded, its changes are saved first.
    * 
    * Params:
    *  name = Design to load. If null, the default will be loaded.
    */
    void loadDesign (char[] name = null) {
        if (changes !is null)	// A design was previously loaded
            save;       // own lock
            
	mutex.lock;
        scope(exit) mutex.unlock;
        
        // Load data (loadData tests if it's already loaded first):
        if (name is null) {
            loadData (false);
            name = defaultDesign;
        } else
            loadData (true);
        
        
        // Get data:
        auto p = name in data;
        while (p is null) {
            if (name == defaultDesign)
                throw new GuiException ("Unable to load [specified or] default design");
            name = defaultDesign;       // try again with the default
            p = name in data;
        }
        curData = *p;
        
        // Get/create a changes section:
        if (changesDS is null)
            changesDS = new mt.DataSet;
        
        mt.IDataSection* q = name in changesDS.sec;
        if (!q || ((changes = cast(WidgetDataChanges) *q) is null)) {
            changes = new WidgetDataChanges (curData);
            changesDS.sec[name] = changes;
        }
        
        // Create the widgets:
        createRootWidget;
    }
    
    /** Save changes, if any exist.
    * 
    * Is run when the manager is destroyed, but could be run at other times too. */
    void save () {
	preSave;
	
        mutex.lock;
        scope(exit) mutex.unlock;
        
        // Make all widgets save any changed data; return if no changes:
        if (!child.saveChanges)
            return;
        
        if (loadUserFile) { // merge entries from user file into current changes
            try {
                scope IReader reader = confDir.makeMTReader (
                fileName, PRIORITY.HIGH_ONLY, changesDS, true);
                
                // Create if necessary, only corresponding to existing designs read:
                reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) {
                    WidgetDataSet* p = id in data;
                    if (p is null)
                        throw new Exception ("File has changed since it was loaded!");
                    return new WidgetDataChanges (*p);
                };
                
                reader.read;
            } catch (NoFileException) {
                // No user file exists; not an error.
            } catch (Exception e) {
                logger.error ("Error reading "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" prior to saving:");
                logger.error (e.msg);
                logger.error ("Overwriting the file.");
                // Continue...
            }
            loadUserFile = false;   // don't need to do it again
        }
        
        try {   // Save
        IWriter writer;
        writer = confDir.makeMTWriter (fileName, changesDS);
        writer.write;
        } catch (Exception e) {
            logger.error ("Saving to "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" failed:");
            logger.error (e.msg);
            // No point in throwing since it doesn't affect anything else.
        }
    }
    
    /** Get the names of all designs available. */
    char[][] designs() {
        synchronized(mutex) {
            loadData (true);
            return data.keys;
        }
    }
    
    /** Called when translation strings have been reloaded. */
    void reloadStrings (Content) {
	Items.loadTranslation;
	child.setup (++setupN, 2);
	child.setWidth  (w, -1);
	child.setHeight (h, -1);
	child.setPosition (0,0);
	requestRedraw;
    }
    
    //BEGIN IParentWidget methods
    // If call reaches the widget manager there isn't any recursion.
    //NOTE: should be override
    final void recursionCheck (widgetID) {}
    //FIXME: implement
    override void minSizeChange (IChildWidget widget, wdim mw, wdim mh) {}
    //END IParentWidget methods
    
    //BEGIN IWidgetManager methods
    override IChildWidget makeWidget (IParentWidget parent, widgetID id, IContent content = null)
    {
        debug assert (parent, "makeWidget: parent is null (code error)");
        debug scope (failure)
                logger.warn ("Creating widget \""~id~"\" failed.");
        
        WidgetData data = curData[id];
        if (data.ints.length < 1) {
            logger.error ("No int data; creating a debug widget");
            data.ints = [WIDGET_TYPE.Debug];
        }
        int type = data.ints[0];    // type is first element of data
    
        try {
            // Statically programmed binary search on type, returning a new widget or calling a
            // function:
            //pragma (msg, binarySearch ("type", WIDGETS));
            mixin (binarySearch ("type", WIDGETS));
            // Not returned a new widget:
            logger.error ("Bad widget type: {}; creating a debug widget instead",type);
        } catch (Exception e) {
            logger.error ("Error creating widget: {}; creating a debug widget instead.", e.msg);
        }
    
        return new DebugWidget (this, this, id, data);
    }
    
    override WidgetData widgetData (widgetID id) {
        return curData[id];
    }
    override void widgetData (widgetID id, WidgetData d) {
        changes[id] = d;		// also updates WidgetDataSet in data.
    }
    
    override wdims dimData (widgetID id) {
        return curData.dims (id);
    }
    override void dimData (widgetID id, wdims d) {
        changes.setDims(id, d);		// also updates WidgetDataSet in data.
    }
    
    // These methods are only intended for use within the gui package. They are not necessarily
    // thread-safe.
    IRenderer renderer () {
        assert (rend !is null, "WidgetManager.renderer: rend is null");
        return rend;
    }
    
    void addPopup (IChildWidget parnt, IChildWidget widg, int flags = 0) {
	debug assert (parnt && widg, "addPopup: null widget");
        if (popups.length >= popupsMem.length)
            popupsMem.length = popupsMem.length * 2 + 2;
        with (popupsMem[popups.length]) {
            parent = parnt;
            widget = widg;
	    w = widg.width;
	    h = widg.height;
            if (flags & 1) {
	    	y = parent.yPos;
            	if (y+h > this.h) y += parent.height - h;
	    	x = parent.xPos + parent.width;
	    	if (x+w > this.w) x = parent.xPos - w;
            } else {
                x = parent.xPos;				// align on left edge
                if (x+w > this.w) x += parent.width - w;	// align on right edge
                y = parent.yPos + parent.height;		// place below
                if (y+h > this.h) y = parent.yPos - h;		// place above
            }
	    widget.setPosition (x, y);
	}
        popups = popupsMem[0..popups.length+1];
	requestRedraw;
    }
    void removePopup (IChildWidget parnt) {
	foreach_reverse (i,popup; popups) {
	    if (popup.parent is parnt)
		popups = popups[0..i];
	}
	requestRedraw;
    }

    void requestRedraw () {
        imde.mainSchedule.request(imde.SCHEDULE.DRAW);
    }
    
    void addClickCallback (bool delegate(wdabs, wdabs, ubyte, bool) dg) {
        clickCallbacks[dg.ptr] = dg;
    }
    void addMotionCallback (void delegate(wdabs, wdabs) dg) {
        motionCallbacks[dg.ptr] = dg;
    }
    void removeCallbacks (void* frame) {
        clickCallbacks.removeKey(frame);
        motionCallbacks.removeKey(frame);
    }
    //END IWidgetManager methods
    
protected:
    /** Second stage of loading the widgets.
    * 
    * loadDesign handles the data; this method needs to:
    * ---
    * // 1. Create the root widget:
    * child = makeWidget ("root");
    * child.setup (0, 3);
    * // 2. Set the size:
    * child.setWidth  (child.minWidth,  1);
    * child.setHeight (child.minHeight, 1);
    * // 3. Set the position (necessary part of initialization):
    * child.setPosition (0,0);
    * ---
    */
    void createRootWidget();
    
    /** Called before saving (usually when the GUI is about to be destroyed, although not
    *  necessarily). */
    void preSave ();
    
public:
    //BEGIN makeWidget metacode
private static {
/// Widget types. Items match widget names without the "Widget" suffix.
enum WIDGET_TYPE : int {
    FUNCTION		= 0x2000,   // Function called instead of widget created (no "Widget" appended to fct name)
    TAKES_CONTENT	= 0x4000,   // Flag indicates widget's this should be passed an IContent reference.
    SAFE_RECURSION	= 0x8000,   // Safe to instantiate recursively without infinite looping.
    
    // Use widget names rather than usual capitals convention
    Unnamed		= 0x0,      // Only for use by widgets not created with createWidget
    
    // blank: 0x1
    FixedBlank		= 0x1,
    SizableBlank	= 0x2,
    Debug		= 0xF,
    
    // popup widgets: 0x10
    PopupMenu		= TAKES_CONTENT | 0x11,
    SubMenu		= TAKES_CONTENT | 0x12,
    
    // labels: 0x20
    ContentLabel	= TAKES_CONTENT | 0x20,
    TextLabel		= 0x21,
    
    // content functions: 0x30
    editContent		= FUNCTION | TAKES_CONTENT | SAFE_RECURSION | 0x30,
    addContent		= FUNCTION | 0x31,
    flatMenuContent	= FUNCTION | TAKES_CONTENT | SAFE_RECURSION | 0x32,
    subMenuContent	= FUNCTION | TAKES_CONTENT | 0x33,
    
    // content widgets: 0x40
    DisplayContent	= TAKES_CONTENT | 0x40,
    BoolContent		= TAKES_CONTENT | 0x41,
    AStringContent	= TAKES_CONTENT | 0x42,
    ButtonContent	= TAKES_CONTENT | 0x43,
    MenuButtonContent	= TAKES_CONTENT | 0x44,
    
    GridLayout		= TAKES_CONTENT | 0x100,
    ContentList		= TAKES_CONTENT | SAFE_RECURSION | 0x110,
    
    FloatingArea	= TAKES_CONTENT | 0x200,
    Switch		= TAKES_CONTENT | 0x210,
}

// Only used for binarySearch algorithm generation; must be ordered by numerical values.
const char[][] WIDGETS = [
        "FixedBlank",
        "SizableBlank",
        "Debug",
	"TextLabel",
	"addContent",
	"PopupMenu",
	"SubMenu",
	"ContentLabel",
        "DisplayContent",
        "BoolContent",
	"AStringContent",
	"ButtonContent",
	"MenuButtonContent",
	"GridLayout",
	"FloatingArea",
	"Switch",
	"subMenuContent",
	"ContentList",
	"editContent",
	"flatMenuContent"];

/* Generates a binary search algorithm for makeWidget. */
char[] binarySearch (char[] var, char[][] consts) {
    if (consts.length > 3) {
        return `if (`~var~` <= WIDGET_TYPE.`~consts[$/2 - 1]~`) {` ~
                binarySearch (var, consts[0 .. $/2]) ~
                `} else {` ~
                binarySearch (var, consts[$/2 .. $]) ~
                `}`;
    } else {
        char[] ret;
        foreach (c; consts) {
            ret ~= `if (` ~ var ~ ` == WIDGET_TYPE.` ~ c ~ `) {
                        debug (mdeWidgets) logger.trace ("Creating new `~c~`.");
                        if (!(WIDGET_TYPE.`~c~` & WIDGET_TYPE.SAFE_RECURSION))
                    	  parent.recursionCheck (id);
                        static if (WIDGET_TYPE.`~c~` & WIDGET_TYPE.FUNCTION)
                          return `~c~` (this, parent, id, data, content);
                        else static if (WIDGET_TYPE.`~c~` & WIDGET_TYPE.TAKES_CONTENT)
                          return new `~c~`Widget (this, parent, id, data, content);
                        else
                          return new `~c~`Widget (this, parent, id, data);
                    } else `;
        }
        ret = ret[0..$-6];	// remove last else
        return ret;
    }
}

debug { // check items in WIDGETS are listed in order
    char[] WIDGETS_check () {
        char[] ret;
        for (int i = WIDGETS.length-2; i > 0; --i) {
            ret ~= "WIDGET_TYPE."~WIDGETS[i] ~" >= WIDGET_TYPE."~ WIDGETS[i+1];
            if (i>1) ret ~= " || ";
        }
        return ret;
    }
    mixin ("static if ("~WIDGETS_check~")
        static assert (false, \"WIDGETS is not in order!\");");
}
}
    //END makeWidget metacode
    
protected:
    // Dataset/design data:
    final char[] fileName;
    char[] defaultDesign;		// The design specified in the file header.
    char[] rendName;			// Name of renderer; for saving and creating renderers
    
    // Loaded data, indexed by design name. May not be loaded for all gui designs:
    scope WidgetDataSet[char[]] data;
    private bool allLoaded = false;	// applies to data
    WidgetDataSet curData;		// Current data
    WidgetDataChanges changes;		// Changes for the current design.
    scope mt.DataSet changesDS;		// changes and sections from user file (used for saving)
    bool loadUserFile = true;		// still need to load user file for saving?
    
    IRenderer rend;
    
    // Widgets:
    wdim w,h;				// area available to the widgets
    wdim mw,mh;				// minimal area required by widgets (ideally for limiting w,h)
    scope IChildWidget child;		// The primary widget.
    uint setupN;			// n to pass to IChildWidget.setup
    
    struct ActivePopup {
        IChildWidget widget;
	IChildWidget parent;
        wdabs x,y;
        wdsize w,h;
    }
    ActivePopup[] popups;	// Pop-up [menus] to draw. Last element is top popup.
    ActivePopup[] popupsMem;	// allocated memory for popups
    // callbacks indexed by their frame pointers. Must support removal of elements in foreach:
    SortedMap!(void*,bool delegate(wdabs cx, wdabs cy, ubyte b, bool state)) clickCallbacks;
    SortedMap!(void*,void delegate(wdabs cx, wdabs cy)) motionCallbacks;
    IChildWidget keyFocus;	// widget receiving keyboard input when non-null
    IChildWidget highlighted;	// NOTE: in some ways should be same as keyFocus
    
    Mutex mutex;			// lock on methods for use outside the package.
}