Mercurial > projects > mde
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; +}