view mde/gui/WidgetManager.d @ 131:9cff74f68b84

Major revisions to popup handling. Buttons can close menus now, plus some smaller impovements. Removed Widget module. Moved Widget.AWidget to AChildWidget.AChildWidget and Widget.AParentWidget to AParentWidget.AParentWidget. Removed ASingleParentWidget to improve code sharing. AChildWidget doesn't implement IParentWidget like AWidget did. New IPopupParentWidget extending IParentWidget for the WM and some widgets to handle popups. Cut old popup management code. New underMouse() function replacing highlight(); called on all widgets. Separate menu-popup and button widgets aren't needed for menus now. Functions returning content widgets have been moved to their own module. Cleaned up jobs.txt. Switched to 80 line length for Ddoc.
author Diggory Hardy <diggory.hardy@gmail.com>
date Wed, 21 Jan 2009 13:01:40 +0000
parents ad91de8867a0
children 264028f4115a
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.
 * 
 * Public non IWidget* methods should be thread-safe.
 *****************************************************************************/
module mde.gui.WidgetManager;

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

import imde = mde.imde;
import mde.lookup.Options;	// miscOpts.L10n callback
import mde.content.Content;
import Items = mde.content.Items;	// loadTranslation
debug import mde.content.miscContent;	// Debug menu

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.contentFunctions;
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;
        miscOpts.L10n.addCallback (&reloadStrings);
        
        clickCallbacks = new typeof(clickCallbacks);
        motionCallbacks = new typeof(motionCallbacks);
        
        debug {
            auto lWS = new EventContent ("logWidgetSize");
            lWS.addCallback (&logWidgetSize);
            imde.menu.append (lWS);
        }
    }
    
    /* 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");
        }
	
        Items.loadTranslation ();
    }
    
    /** 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;
        underMouse = child;	// must be something
    }
    
    /** 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:
        child.saveChanges;
        
        if (changes.noChanges)
            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. */
    protected void reloadStrings (Content) {
	Items.loadTranslation;
	child.setup (++setupN, 2);
	child.setWidth  (w, -1);
	child.setHeight (h, -1);
	child.setPosition (0,0);
	requestRedraw;
    }
    
    // These methods are only intended for use within the gui package.
    // They are not necessarily thread-safe:
    
    //BEGIN IParentWidget methods
    // If call reaches the widget manager there isn't any recursion.
    //NOTE: should be override
    final void recursionCheck (widgetID) {}
    
    override void minWChange (IChildWidget widget, wdim nmw) {
        debug assert (widget is child, "WM.mSC (code error)");
        mw = nmw;
        if (w < nmw) {
            child.setWidth (nmw, -1);
            w = nmw;
        }
        child.setPosition (0,0);
        requestRedraw;
    }
    override void minHChange (IChildWidget widget, wdim nmh) {
        debug assert (widget is child, "WM.mSC (code error)");
        mh = nmh;
        if (h < nmh) {
            child.setHeight (nmh, -1);
            h = nmh;
        }
        child.setPosition (0,0);
        requestRedraw;
    }
    //END IParentWidget methods
    
    //BEGIN IPopupParentWidget methods
    override IPopupParentWidget getParentIPPW () {
        return this;
    }
    
    override void addChildIPPW (IPopupParentWidget ippw) {
        if (childIPPW)
            childIPPW.removedIPPW;
        childIPPW = ippw;
        requestRedraw;
    }
    override bool removeChildIPPW (IPopupParentWidget ippw) {
        if (childIPPW !is ippw) return false;
        childIPPW.removedIPPW;
        childIPPW = null;
        mAIPPW = false;
        requestRedraw;
        return false;
    }
    
    override void menuActive (bool mA) {
        mAIPPW = mA;
        if (childIPPW)
            childIPPW.menuActive = mA;
    }
    override bool menuActive () {
        return mAIPPW;
    }
    
    // Don't do anything. E.g. can get called by non-popup buttons.
    override void menuDone () {}
    
    override IChildWidget getPopupWidget (wdabs cx, wdabs cy, bool closePopup) {
        IChildWidget ret;
        if (childIPPW) {
            ret = childIPPW.getPopupWidget (cx, cy, closePopup);
            if (closePopup && ret is null) {
                menuActive = false;
                removeChildIPPW (childIPPW);
            }
        }
        return ret;
    }
    
    debug protected override bool isChild (IPopupParentWidget ippw) {
        return ippw is childIPPW;
    }
    
    override void removedIPPW () {}	// irrelevant
    override void drawPopup () {}
    //END IPopupParentWidget 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.
    }
    
    IRenderer renderer () {
        assert (rend !is null, "WidgetManager.renderer: rend is null");
        return rend;
    }
    
    void positionPopup (IChildWidget parent, IChildWidget popup, int flags = 0) {
	debug assert (parent && popup, "positionPopup: null widget");
        wdim w = popup.width,
             h = popup.height,
             x, y;
        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
        }
        popup.setPosition (x, y);
    }

    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
    
    debug void logWidgetSize (Content) {
        logger.trace ("size: {,4},{,4}; minimal: {,4},{,4} - WidgetManager", w,h, mw,mh);
        child.logWidgetSize;
    }
    
protected:
    void updateUnderMouse (wdabs cx, wdabs cy, bool closePopup) {
        auto oUM = underMouse;
        underMouse = getPopupWidget (cx, cy, closePopup);
        if (underMouse is null) {
            debug assert (child.onSelf (cx, cy), "WidgetManager: child doesn't cover whole area");
            underMouse = child.getWidget (cx, cy);
        }
        if (underMouse !is oUM) {
            debug assert (oUM && underMouse, "no widget under mouse: error");
            oUM.underMouse (false);
            underMouse.underMouse (true);
        }
    }
    
    /** 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,
    
    // 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,
    
    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",
	"ContentLabel",
        "DisplayContent",
        "BoolContent",
	"AStringContent",
	"ButtonContent",
	"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;				// current widget size; should be at least (mw,mh) even if not displayable
    wdim mw,mh;				// minimal area required by widgets
    scope IChildWidget child;		// The primary widget.
    uint setupN;			// n to pass to IChildWidget.setup
    
    bool mAIPPW;			// IPPW variable
    IPopupParentWidget childIPPW;	// child IPPW, if any active
    
    // 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
    IChildWidget underMouse;	// widget under the mouse pointer
    
    Mutex mutex;			// lock on methods for use outside the package.
}