diff mde/gui/WidgetData.d @ 75:25cb7420dc91

A massive overhaul/rewrite for the gui's data management and setup code. Currently much that was working is broken. imde's classes are created in a static this instead of mde's main. gl setup code moved from gl/basic.d to gl/draw.d mergetag.DefaultData: now HIGH_LOW priority instead of LOW_HIGH. Reduced type list to only used types; small fix for indent function. setup.paths: new NoFileException thrown instead of MTFileIOException
author Diggory Hardy <diggory.hardy@gmail.com>
date Mon, 28 Jul 2008 18:17:48 +0100
parents
children 65780e0e48e6
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/gui/WidgetData.d	Mon Jul 28 18:17:48 2008 +0100
@@ -0,0 +1,351 @@
+/* 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.Arg!(char[]);
+            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.Arg!(char[]);
+            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;
+}