view mde/gui/WidgetData.d @ 76:65780e0e48e6

Re-enabled click event passing in the gui to make ButtonWidget work. Bugfix (pass void* not class reference). Change to allow compilation with dmd 1.027 (don't use DefaultData's Arg!() template).
author Diggory Hardy <diggory.hardy@gmail.com>
date Mon, 28 Jul 2008 18:49:18 +0100
parents 25cb7420dc91
children 3dfd934100f7
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/>. */

/*************************************************************************************************
 * Code to manage the data used to create widgets and save changes to it.
 * 
 * When loading, a WidgetDataSet instance is loaded from file and its data used to create the
 * widgets. An empty WidgetDataChanges instance is also created.
 * 
 * If any data requires changing, it is added to the WidgetDataChanges instance, which also
 * updates the the WidgetDataSet instance used to load the widgets (in case of a re-load from this
 * data). When the data should be saved, if the WidgetDataChanges instance is not empty, data from
 * the highest priority (i.e. the user) file is merged into it, preserving both the current
 * changes and previous changes saved to the use file, before saving to the user file.
 *************************************************************************************************/
module mde.gui.WidgetData;

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

// For loading from file:
import mt = mde.mergetag.DataSet;
import mt = mde.mergetag.DefaultData;
import mt = mde.mergetag.exception;
import mde.mergetag.Reader;
import mde.mergetag.Writer;
import mde.setup.paths;
import mde.mergetag.parse.parseTo;
import mde.mergetag.parse.parseFrom : parseFrom;

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

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


/*************************************************************************************************
 * 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;
    }
    ~this () {
        save;
    }
    
    /* 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
        
        if (!confDir.exists (fileName)) {
            logger.error ("Unable to load GUI: no config file!");
            return; // not a fatal error (so long as the game can run without a GUI!)
        }
        
        // 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 (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:
        curData = data[name]; // NOTE: may throw
        
        // Get/create a changes section:
        if (changesDS is null)
            changesDS = new mt.DataSet;
        
        mt.IDataSection* p = name in changesDS.sec;
        if (p && ((changes = cast(WidgetDataChanges) *p) !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;
        
        if (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;
        }
    }
    
    
    /** Create a widget by ID. */
    IChildWidget makeWidget (widgetID id) {
        return createWidget (this, curData[id]);
    }
    
    /** For making changes. */
    void setData (widgetID id, WidgetData d) {
        noChanges = false;
        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);
     * ---
     */
    // FIXME: abstract?
    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?
    bool noChanges = true;      // do we have any changes to save?
    
    scope IChildWidget child;   // The primary widget.
    
    Mutex mutex;    // lock on methods for use outside the package.
}


package:
/*************************************************************************************************
 * Contains data for all widgets in a GUI.
 *************************************************************************************************/
class WidgetDataSet : mt.IDataSection
{
    //BEGIN Mergetag code
    void addTag (char[] tp, mt.ID id, char[] dt) {
        // Priority is HIGH_LOW. Only load tag if it doesn't already exist.
        if (tp == "WidgetData" && (id in widgetData) is null) {
            // Note: is a very simple form of struct deserialization
            WidgetData data;
            with(data) {
                char[][] strs = split (dt);
                if (strs.length != 2)
                    throw new ParseException ("Not two components");
                ints = parseTo!(int[]) (strs[0]);
                str = parseTo!(char[]) (strs[1]);
            }
            widgetData[id] = data;
        }
    }
    // Only WidgetDataChanges is used for writing.
    void writeAll (ItemDelg dlg) {}
    //END Mergetag code
    
    /** Get the widget data for widget i. */
    WidgetData opIndex (widgetID i) {
        return widgetData[i];
    }
    
    // Per-widget data:
    protected WidgetData[widgetID] widgetData;
}

/*************************************************************************************************
 * Contains changes to widget data.
 * 
 * Can be read as normal and written.
 *************************************************************************************************/
class WidgetDataChanges : WidgetDataSet
{
    /**
     * Params:
     *  wds = The base WidgetDataSet these changes are applied against. */
    this (WidgetDataSet wds) {
        base = wds;
    }
    
    //BEGIN Mergetag code
    // HIGH_LOW priority of addTag allows existing entries (i.e. the changes) to be preserved while
    // other entries are read from files.
    void writeAll (ItemDelg dlg) {
        foreach (id,data; widgetData) {
            // Note: is a very simple form of struct serialization
            with(data) {
                dlg ("WidgetData", id, 
                     parseFrom!(int[]) (ints) ~ ',' ~ parseFrom!(char[]) (str) );
            }
        }
    }
    //END Mergetag code
    
    /** Set the widget data for widget i.
     */
    void opIndexAssign (WidgetData d, widgetID i) {
        widgetData[i] = d;
        base.widgetData[i] = d;
    }
    
    /** Do any changes exist? True if no changes have been stored. */
    bool none () {
        return widgetData.length == 0;
    }
    
    protected WidgetDataSet base;
}