view mde/gui/WidgetManager.d @ 105:08651e8a8c51

Quit button, big changes to content system. Moved mde.gui.content to mde.content to reflect it's not only used by the gui. Split Content module into Content and AStringContent. New AContent and EventContent class. Callbacks are now generic and implemented in AContent. Renamed TextContent to StringContent and ValueContent to AStringContent.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 29 Nov 2008 12:36:39 +0000
parents 42e241e7be3e
children c9fc2d303178
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.
 *
 * This is the module to use externally to create a graphical user interface (likely also with
 * content modules).
 *************************************************************************************************/
module mde.gui.WidgetManager;

import mde.gui.WidgetDataSet;
import mde.gui.widget.Ifaces;
import mde.gui.renderer.createRenderer;

// For adding the input event callbacks and requesting redraws:
import imde = mde.imde;
import mde.input.Input;
import mde.scheduler.Scheduler;
import mde.setup.Screen;

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

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

/*************************************************************************************************
 * The widget manager.
 * 
 * This is responsible for loading and saving an entire gui (although more than one may exist),
 * controlling the rendering device (e.g. the screen or a texture), and providing user input.
 * 
 * Currently mouse coordinates are passed to widgets untranslated. It may make sense to translate
 * them and possibly drop events for some uses, such as if the gui is drawn to a texture.
 * 
 * Aside from the IWidgetManager methods, this class should be thread-safe.
 *************************************************************************************************/
class WidgetManager : WidgetLoader, Screen.IDrawable {
    /** Construct a new widget manager.
     * 
     * params:
     *  fileName = Name of file specifying the gui, excluding path and extension.
     */
    this (char[] file) {
        super(file);
        
        Screen.addDrawable (this);
    }
    
    // this() runs during static this(), when imde.input doesn't exist. init() runs later.
    void init () {
        // Doesn't need a lock - cannot conflict with other class functions.
        // Events we want to know about:
        imde.input.addMouseClickCallback(&clickEvent);
        imde.input.addMouseMotionCallback(&motionEvent);
    }
    
    
    /** Draw the gui. */
    void draw() {
        synchronized(mutex)
            if (child)
                child.draw;
    }
    
    
    /** For mouse click events.
     *
     * Sends the event on to the relevant windows and all click callbacks. */
    void clickEvent (ushort cx, ushort cy, ubyte b, bool state) {
        debug scope (failure)
            logger.warn ("clickEvent: failed!");
        mutex.lock;
        scope(exit) mutex.unlock;
        if (child is null) return;
        
        // NOTE: buttons receive the up-event even when drag-callbacks are in place.
        foreach (dg; clickCallbacks)
            // See IWidgetManager.addClickCallback's documentation:
            if (dg (cast(wdabs)cx, cast(wdabs)cy, b, state)) return;
        
        // test the click was on the child widget
        // cx/cy are unsigned, thus >= 0. Widget starts at (0,0)
        if (cx >= child.width || cy >= child.height) {
            debug logger.warn ("WidgetManager received click not on child; potentially an error");
            return;
        }
        IChildWidget widg = child.getWidget (cast(wdabs)cx,cast(wdabs)cy);
        //debug logger.trace ("Click on {}", widg);
	if (keyFocus && keyFocus !is widg) {
	    keyFocus.keyFocusLost;
	    keyFocus = null;
	    imde.input.setLetterCallback (null);
	}
        if (widg !is null) {
	    if (widg.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state) & 1) {
		keyFocus = widg;
		imde.input.setLetterCallback (&widg.keyEvent);
	    }
	}
    }
    
    /** For mouse motion events.
     *
     * Sends the event on to all motion callbacks. */
    void motionEvent (ushort cx, ushort cy) {
        debug scope (failure)
                logger.warn ("motionEvent: failed!");
        mutex.lock;
        scope(exit) mutex.unlock;
        
        foreach (dg; motionCallbacks)
            dg (cast(wdabs)cx, cast(wdabs)cy);
    }
    
    
    void sizeEvent (int nw, int nh) {   // IDrawable function
        mutex.lock;
        scope(exit) mutex.unlock;
        
        w = cast(wdim) nw;
        h = cast(wdim) nh;
        
        if (w < mw || h < mh)
            logger.warn ("Minimal dimensions ({},{}) not met: ({},{}), but I cannot resize myself!",mw,mh,w,h);
        
        if (!child) return;     // if not created yet.
        child.setWidth  (w, -1);
        child.setHeight (h, -1);
        child.setPosition (0,0);
    }
    
    //BEGIN IWidgetManager methods
    // 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 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.remove(frame);
        motionCallbacks.remove(frame);
    }
    //END IWidgetManager methods
    
protected:
    /* Second stage of widget loading.
     * Note: sizeEvent should be called with window size before this. */
    void createRootWidget () {
        // The renderer needs to be created on the first load, but not after this.
        if (rend is null)
            rend = createRenderer (rendName);
        
        child = makeWidget ("root");
        finalize;
        
        mw = child.minWidth;
        mh = child.minHeight;
        
        if (w < mw || h < mh)
            logger.warn ("Minimal dimensions ({},{}) not met: ({},{}), but I cannot resize myself!",mw,mh,w,h);
        
        child.setWidth  (w, -1);
        child.setHeight (h, -1);
        child.setPosition (0,0);
    }
    
    void preSave () {
	if (keyFocus) {
	    keyFocus.keyFocusLost;
	    keyFocus = null;
	    imde.input.setLetterCallback (null);
	}
    }
    
private:
    IRenderer rend;
    wdim w,h;       // area available to the widgets
    wdim mw,mh;     // minimal area available to the widgets
    // callbacks indexed by their frame pointers:
    bool delegate(wdabs cx, wdabs cy, ubyte b, bool state) [void*] clickCallbacks;
    void delegate(wdabs cx, wdabs cy) [void*] motionCallbacks;
    IChildWidget keyFocus;	// widget receiving keyboard input when non-null
}


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

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

/*************************************************************************************************
* Contains the code for loading and saving the gui, 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 WidgetLoader : 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!");
            // 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;
        }
    }
    
    IChildWidget makeWidget (widgetID id, IContent content = null) {
        debug (mdeWidgets) logger.trace ("Creating widget \""~id~'"');
        return createWidget (this, id, curData[id], content);
    }
    IChildWidget makeWidget (widgetID id, WidgetData data, IContent content = null) {
	debug (mdeWidgets) logger.trace ("Creating widget \""~id~'"');
	return createWidget (this, id, data, content);
    }
    
    /** Runs finalize for all descendants, in a deepest first order. */
    /* NOTE: The way this function works may seem a little odd, but it's designed to allow
     * shared alignments to be initialized properly:
     * 1. all sharing members need to know their children's min size
     * 2. all sharing members need to add their children's min size to the alignment
     * 3. all sharing members can only then get their min size
     * This method will fail if alignment members are not all of the same generation. An alternate
     * method without this drawback would be to have shared alignments created with a list of
     * pointers to their members, and once all members have been created the alignment could
     * initialize itself, first making sure each members' children have been initialized. */
    void finalize () {
        IChildWidget[][] descendants;   // first index: depth; is a list of widgets at each depth
        
        void recurseChildren (size_t depth, IChildWidget widget) {
            foreach (child; widget.children)
                recurseChildren (depth+1, child);
            
            if (descendants.length <= depth)
                descendants.length = depth * 2 + 1;
            descendants[depth] ~= widget;
        }
        
        recurseChildren (0, child);
        foreach_reverse (generation; descendants) {
            foreach (widget; generation)
                widget.prefinalize;
            foreach (widget; generation)
                widget.finalize;
        }
    }
    
    wdims dimData (widgetID id) {
        return curData.dims (id);
    }
    void setData (widgetID id, WidgetData d) {
        changes[id] = d;        // also updates WidgetDataSet in data.
    }
    void setDimData (widgetID id, wdims d) {
        changes.setDims(id, d);    // also updates WidgetDataSet in data.
    }
    
    /** Second stage of loading the widgets.
    * 
    * loadDesign handles the data; this method needs to:
    * ---
    * // 1. Create the root widget:
    * child = makeWidget ("root");
    * finalize;
    * // 2. Set the setSize, e.g.:
    * child.setWidth  (child.minWidth,  1);
    * child.setHeight (child.minHeight, 1);
    * ---
    */
    void createRootWidget();
    
    /** Called before saving (usually when the GUI is about to be destroyed, although not
     *  necessarily). */
    void preSave () {}
    
protected:
    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?
    
    scope IChildWidget child;		// The primary widget.
    
    Mutex mutex;			// lock on methods for use outside the package.
}