view mde/gui/WidgetManager.d @ 91:4d5d53e4f881

Shared alignment for dynamic content lists - finally implemented! Lots of smaller changes too. Some debugging improvements. When multiple .mtt files are read for merging, files with invalid headers are ignored and no error is thrown so long as at least one file os valid.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 16 Oct 2008 17:43:48 +0100
parents b525ff28774b
children 085f2ca31914
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);
    }
    
    // NOTE - temporarily here to allow CTOR to run safely during static this
    // called during init
    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");
        
        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.
     * FIXME - 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);
    }
    
    /** 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");
    * // 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.
}