comparison 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
comparison
equal deleted inserted replaced
74:cee261eba249 75:25cb7420dc91
1 /* LICENSE BLOCK
2 Part of mde: a Modular D game-oriented Engine
3 Copyright © 2007-2008 Diggory Hardy
4
5 This program is free software: you can redistribute it and/or modify it under the terms
6 of the GNU General Public License as published by the Free Software Foundation, either
7 version 2 of the License, or (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
10 without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License
14 along with this program. If not, see <http://www.gnu.org/licenses/>. */
15
16 /*************************************************************************************************
17 * Code to manage the data used to create widgets and save changes to it.
18 *
19 * When loading, a WidgetDataSet instance is loaded from file and its data used to create the
20 * widgets. An empty WidgetDataChanges instance is also created.
21 *
22 * If any data requires changing, it is added to the WidgetDataChanges instance, which also
23 * updates the the WidgetDataSet instance used to load the widgets (in case of a re-load from this
24 * data). When the data should be saved, if the WidgetDataChanges instance is not empty, data from
25 * the highest priority (i.e. the user) file is merged into it, preserving both the current
26 * changes and previous changes saved to the use file, before saving to the user file.
27 *************************************************************************************************/
28 module mde.gui.WidgetData;
29
30 import mde.gui.exception;
31 import mde.gui.widget.Ifaces;
32 import mde.gui.widget.createWidget;
33
34 // For loading from file:
35 import mt = mde.mergetag.DataSet;
36 import mt = mde.mergetag.DefaultData;
37 import mt = mde.mergetag.exception;
38 import mde.mergetag.Reader;
39 import mde.mergetag.Writer;
40 import mde.setup.paths;
41 import mde.mergetag.parse.parseTo;
42 import mde.mergetag.parse.parseFrom : parseFrom;
43
44 import tango.core.sync.Mutex;
45 import tango.util.log.Log : Log, Logger;
46
47 private Logger logger;
48 static this () {
49 logger = Log.getLogger ("mde.gui.WidgetData");
50 }
51
52
53 /*************************************************************************************************
54 * Contains the code for loading and saving the gui, but not the code for drawing it or handling
55 * user input.
56 *
57 * This abstract class exists solely for separating out some of the functionality.
58 *************************************************************************************************/
59 abstract scope class WidgetLoader : IWidgetManager
60 {
61 /** Construct a new widget loader.
62 *
63 * params:
64 * fileName = Name of file specifying the gui, excluding path and extension.
65 */
66 protected this (char[] file) {
67 mutex = new Mutex; // Used on functions intended to be called from outside the gui package.
68 fileName = file;
69 }
70 ~this () {
71 save;
72 }
73
74 /* Load the widgets' data from the file specified to the CTOR.
75 *
76 * params:
77 * allDesigns = Load all sections
78 */
79 private void loadData (bool allDesigns = false) {
80 if (allLoaded || (defaultDesign !is null && allDesigns == false))
81 return; // test if already loaded
82
83 if (!confDir.exists (fileName)) {
84 logger.error ("Unable to load GUI: no config file!");
85 return; // not a fatal error (so long as the game can run without a GUI!)
86 }
87
88 // Set up a reader
89 scope IReader reader;
90 try {
91 reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_LOW, null, true);
92
93 // Read from the HEADER:
94 // Get the renderer
95 char[]* p = "Renderer" in reader.dataset.header.Arg!(char[]);
96 if (p is null || *p is null) {
97 logger.warn ("No renderer specified: using \"Simple\"");
98 rendName = "Simple";
99 }
100 else
101 rendName = *p;
102
103 // Get which section to use
104 p = "Design" in reader.dataset.header.Arg!(char[]);
105 if (p is null || *p is null) {
106 logger.warn ("No gui design specified: trying \"Default\"");
107 defaultDesign = "Default";
108 }
109 else
110 defaultDesign = *p;
111
112 // Read the body:
113 // Load the chosen design
114 reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) {
115 WidgetDataSet* p = id in data;
116 if (p is null) {
117 data[id] = new WidgetDataSet;
118 return *(id in data);
119 }
120 return *p;
121 };
122
123 if (allDesigns) {
124 reader.read;
125 allLoaded = true;
126 } else
127 reader.read([defaultDesign]);
128 } catch (Exception e) {
129 logger.error ("Unable to load GUI: errors parsing config file ("~confDir.getFileName(fileName,PRIORITY.HIGH_LOW)~"):");
130 logger.error (e.msg);
131 throw new GuiException ("Failure parsing config file");
132 }
133 }
134
135 /** Load the gui from some design.
136 *
137 * If a design was previously loaded, its changes are saved first.
138 *
139 * Params:
140 * name = Design to load. If null, the default will be loaded.
141 */
142 void loadDesign (char[] name = null) {
143 if (changes !is null)
144 save; // own lock
145
146 mutex.lock;
147 scope(exit) mutex.unlock;
148
149 // Load data (loadData tests if it's already loaded first):
150 if (name is null) {
151 loadData (false);
152 name = defaultDesign;
153 } else
154 loadData (true);
155
156
157 // Get data:
158 curData = data[name]; // NOTE: may throw
159
160 // Get/create a changes section:
161 if (changesDS is null)
162 changesDS = new mt.DataSet;
163
164 mt.IDataSection* p = name in changesDS.sec;
165 if (p && ((changes = cast(WidgetDataChanges) *p) !is null)) {}
166 else {
167 changes = new WidgetDataChanges (curData);
168 changesDS.sec[name] = changes;
169 }
170
171 // Create the widgets:
172 createRootWidget;
173 }
174
175 /** Save changes, if any exist.
176 *
177 * Is run when the manager is destroyed, but could be run at other times too. */
178 void save () {
179 mutex.lock;
180 scope(exit) mutex.unlock;
181
182 if (noChanges)
183 return;
184
185 if (loadUserFile) { // merge entries from user file into current changes
186 try {
187 scope IReader reader = confDir.makeMTReader (
188 fileName, PRIORITY.HIGH_ONLY, changesDS, true);
189
190 // Create if necessary, only corresponding to existing designs read:
191 reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) {
192 WidgetDataSet* p = id in data;
193 if (p is null)
194 throw new Exception ("File has changed since it was loaded!");
195 return new WidgetDataChanges (*p);
196 };
197
198 reader.read;
199 } catch (NoFileException) {
200 // No user file exists; not an error.
201 } catch (Exception e) {
202 logger.error ("Error reading "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" prior to saving:");
203 logger.error (e.msg);
204 logger.error ("Overwriting the file.");
205 // Continue...
206 }
207 loadUserFile = false; // don't need to do it again
208 }
209
210 try { // Save
211 IWriter writer;
212 writer = confDir.makeMTWriter (fileName, changesDS);
213 writer.write;
214 } catch (Exception e) {
215 logger.error ("Saving to "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" failed:");
216 logger.error (e.msg);
217 // No point in throwing since it doesn't affect anything else.
218 }
219 }
220
221 /** Get the names of all designs available. */
222 char[][] designs() {
223 synchronized(mutex) {
224 loadData (true);
225 return data.keys;
226 }
227 }
228
229
230 /** Create a widget by ID. */
231 IChildWidget makeWidget (widgetID id) {
232 return createWidget (this, curData[id]);
233 }
234
235 /** For making changes. */
236 void setData (widgetID id, WidgetData d) {
237 noChanges = false;
238 changes[id] = d; // also updates WidgetDataSet in data.
239 }
240
241 /** Second stage of loading the widgets.
242 *
243 * loadDesign handles the data; this method needs to:
244 * ---
245 * // 1. Create the root widget:
246 * child = makeWidget ("root");
247 * // 2. Set the setSize, e.g.:
248 * child.setWidth (child.minWidth, 1);
249 * child.setHeight (child.minHeight, 1);
250 * ---
251 */
252 // FIXME: abstract?
253 void createRootWidget();
254
255 protected:
256 final char[] fileName;
257 char[] defaultDesign; // The design specified in the file header.
258 char[] rendName; // Name of renderer; for saving and creating renderers
259
260 // Loaded data, indexed by design name. May not be loaded for all gui designs:
261 scope WidgetDataSet[char[]] data;
262 private bool allLoaded = false; // applies to data
263 WidgetDataSet curData; // Current data
264 WidgetDataChanges changes; // Changes for the current design.
265 scope mt.DataSet changesDS; // changes and sections from user file (used for saving)
266 bool loadUserFile = true; // still need to load user file for saving?
267 bool noChanges = true; // do we have any changes to save?
268
269 scope IChildWidget child; // The primary widget.
270
271 Mutex mutex; // lock on methods for use outside the package.
272 }
273
274
275 package:
276 /*************************************************************************************************
277 * Contains data for all widgets in a GUI.
278 *************************************************************************************************/
279 class WidgetDataSet : mt.IDataSection
280 {
281 //BEGIN Mergetag code
282 void addTag (char[] tp, mt.ID id, char[] dt) {
283 // Priority is HIGH_LOW. Only load tag if it doesn't already exist.
284 if (tp == "WidgetData" && (id in widgetData) is null) {
285 // Note: is a very simple form of struct deserialization
286 WidgetData data;
287 with(data) {
288 char[][] strs = split (dt);
289 if (strs.length != 2)
290 throw new ParseException ("Not two components");
291 ints = parseTo!(int[]) (strs[0]);
292 str = parseTo!(char[]) (strs[1]);
293 }
294 widgetData[id] = data;
295 }
296 }
297 // Only WidgetDataChanges is used for writing.
298 void writeAll (ItemDelg dlg) {}
299 //END Mergetag code
300
301 /** Get the widget data for widget i. */
302 WidgetData opIndex (widgetID i) {
303 return widgetData[i];
304 }
305
306 // Per-widget data:
307 protected WidgetData[widgetID] widgetData;
308 }
309
310 /*************************************************************************************************
311 * Contains changes to widget data.
312 *
313 * Can be read as normal and written.
314 *************************************************************************************************/
315 class WidgetDataChanges : WidgetDataSet
316 {
317 /**
318 * Params:
319 * wds = The base WidgetDataSet these changes are applied against. */
320 this (WidgetDataSet wds) {
321 base = wds;
322 }
323
324 //BEGIN Mergetag code
325 // HIGH_LOW priority of addTag allows existing entries (i.e. the changes) to be preserved while
326 // other entries are read from files.
327 void writeAll (ItemDelg dlg) {
328 foreach (id,data; widgetData) {
329 // Note: is a very simple form of struct serialization
330 with(data) {
331 dlg ("WidgetData", id,
332 parseFrom!(int[]) (ints) ~ ',' ~ parseFrom!(char[]) (str) );
333 }
334 }
335 }
336 //END Mergetag code
337
338 /** Set the widget data for widget i.
339 */
340 void opIndexAssign (WidgetData d, widgetID i) {
341 widgetData[i] = d;
342 base.widgetData[i] = d;
343 }
344
345 /** Do any changes exist? True if no changes have been stored. */
346 bool none () {
347 return widgetData.length == 0;
348 }
349
350 protected WidgetDataSet base;
351 }