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