view mde/gui/WidgetManager.d @ 92:085f2ca31914

Shared alignments supported in more complex cases.
author Diggory Hardy <diggory.hardy@gmail.com>
date Tue, 21 Oct 2008 09:57:19 +0100
parents 4d5d53e4f881
children 08a4ae11454b
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);
        if (widg !is null)
            widg.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state);
    }
    
    /** 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;
        
        debug logger.trace ("Resize to: {},{}", nw, 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);
    }
    
private:
    // 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;
    IRenderer rend;
    wdim w,h;       // area available to the widgets
    wdim mw,mh;     // minimal area available to the widgets
}


import mde.gui.exception;
import mde.gui.widget.Ifaces;
import mde.gui.content.Content; //NOTE - maybe move IContent to a separate module
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)
            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)) {}
        else {
            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 () {
        mutex.lock;
        scope(exit) mutex.unlock;
        
        // Make all widgets save any changed data; return if no changes:
        if (!child.saveChanges ("root"))
            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;
        }
    }
    
    
    /** Create a widget by ID.
     *
     * A widget instance is created from data found under ID. Multiple instances may be created.
     * NOTE - data conflicts when saving?
     *
     * An IContent may be passed. This could contain many things, e.g. some basic data, a widget,
     * multiple sub-IContents. It is only passed to the widget by createWidget if it's enumeration
     * given in that module has the flag TAKES_CONTENT. */
    IChildWidget makeWidget (widgetID id, IContent content = null) {
        debug (mdeWidgets) logger.trace ("Creating widget \""~id~'"');
        return createWidget (this, id, curData[id], 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;
        }
    }
    
    /** For making changes. */
    void setData (widgetID id, WidgetData d) {
        changes[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();
    
    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.
}