Mercurial > projects > mde
comparison mde/Options.d @ 32:316b0230a849
Lots more work on the GUI. Also renamed lots of files.
Lots of changes to the GUI. Renderer is now used exclusively for rendering and WidgetDecoration is gone.
Renamed lots of files to conform to case policies.
committer: Diggory Hardy <diggory.hardy@gmail.com>
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Wed, 30 Apr 2008 18:05:56 +0100 |
parents | |
children | 57d000574d75 |
comparison
equal
deleted
inserted
replaced
31:baa87e68d7dc | 32:316b0230a849 |
---|---|
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 /** This module handles stored options, currently all except input maps. | |
17 * | |
18 * The purpose of having all options centrally controlled is to allow generic handling by the GUI | |
19 * and ease saving and loading of values. The Options class is only really designed around handling | |
20 * small numbers of variables for now. | |
21 */ | |
22 module mde.Options; | |
23 | |
24 import mde.exception; | |
25 | |
26 import mde.mergetag.Reader; | |
27 import mde.mergetag.Writer; | |
28 import mde.mergetag.DataSet; | |
29 import mde.mergetag.exception; | |
30 import mde.resource.paths; | |
31 | |
32 import tango.scrapple.text.convert.parseTo : parseTo; | |
33 import tango.scrapple.text.convert.parseFrom : parseFrom; | |
34 | |
35 import tango.core.Exception : ArrayBoundsException; | |
36 import tango.util.log.Log : Log, Logger; | |
37 | |
38 /** Base class for handling options. | |
39 * | |
40 * This class itself handles no options and should not be instantiated, but provides a sub-classable | |
41 * base for generic options handling. Also, the static portion of this class tracks sub-class | |
42 * instances and provides loading and saving methods. | |
43 * | |
44 * Each sub-class provides named variables for maximal-speed reading. Local sub-class references | |
45 * should be used for reading variables, and via the addOptionsClass() hook will be loaded from | |
46 * files during pre-init (init0 stage). Do not write changes directly to the subclasses or they will | |
47 * not be saved; use, for example, Options.setBool(...). | |
48 * | |
49 * Details: Options sub-classes hold associative arrays of pointers to all option variables, with a | |
50 * char[] id. This list is used for saving, loading and to provide generic GUI options screens. The | |
51 * built-in support in Options is only for bool, int and char[] types (a float type may get added). | |
52 * Further to this, a generic class is used to store all options which have been changed, and if any | |
53 * have been changed, is merged with options from the user conf dir and saved on exit. | |
54 */ | |
55 class Options : IDataSection | |
56 { | |
57 // No actual options are stored by this class. However, much of the infrastructure is | |
58 // present since it need not be redefined in sub-classes. | |
59 | |
60 // The "pointer lists": | |
61 protected bool* [ID] optsBool; | |
62 protected char[]*[ID] optsCharA; | |
63 protected int* [ID] optsInt; | |
64 | |
65 //BEGIN Mergetag loading/saving code | |
66 void addTag (char[] tp, ID id, char[] dt) { | |
67 if (tp == "bool") { | |
68 bool** p = id in optsBool; | |
69 if (p !is null) **p = parseTo!(bool) (dt); | |
70 } else if (tp == "char[]") { | |
71 char[]** p = id in optsCharA; | |
72 if (p !is null) **p = parseTo!(char[]) (dt); | |
73 } else if (tp == "int") { | |
74 int** p = id in optsInt; | |
75 if (p !is null) **p = parseTo!(int) (dt); | |
76 } | |
77 } | |
78 void writeAll (ItemDelg dlg) { | |
79 foreach (ID id, bool* val; optsBool) dlg ("bool" , id, parseFrom!(bool ) (*val)); | |
80 foreach (ID id, char[]* val; optsCharA) dlg ("char[]", id, parseFrom!(char[]) (*val)); | |
81 foreach (ID id, int* val; optsInt) dlg ("int" , id, parseFrom!(int ) (*val)); | |
82 } | |
83 //END Mergetag loading/saving code | |
84 | |
85 //BEGIN Static | |
86 /** Add an options sub-class to the list for loading and saving. | |
87 * | |
88 * Call from static this() (before Init calls load()). */ | |
89 static void addOptionsClass (Options c, char[] i) | |
90 in { // Trap a couple of potential coding errors: | |
91 assert (c !is null); // Instance must be created before calling addOptionsClass | |
92 assert (((cast(ID) i) in subClasses) is null); // Don't allow a silent replacement | |
93 } body { | |
94 subClasses[cast(ID) i] = c; | |
95 subClassChanges[cast(ID) i] = new OptionsGeneric; | |
96 } | |
97 | |
98 /** Set option symbol of Options class subClass to val. | |
99 * | |
100 * Due to the way options are handled generically, string IDs must be used to access the options | |
101 * via hash-maps, which is a little slower than direct access but necessary since the option | |
102 * must be changed in two separate places. */ | |
103 private static const ERR_MSG = "Options.setXXX called with incorrect parameters!"; | |
104 static void setBool (char[] subClass, char[] symbol, bool val) { | |
105 changed = true; // something got set (don't bother checking this isn't what it already was) | |
106 | |
107 try { | |
108 *(subClasses[cast(ID) subClass].optsBool[cast(ID) symbol]) = val; | |
109 subClassChanges[cast(ID) subClass].setBool (cast(ID) symbol, val); | |
110 } catch (ArrayBoundsException) { | |
111 // log and ignore: | |
112 logger.error (ERR_MSG); | |
113 } | |
114 } | |
115 static void setInt (char[] subClass, char[] symbol, int val) { | |
116 changed = true; // something got set (don't bother checking this isn't what it already was) | |
117 | |
118 try { | |
119 *(subClasses[cast(ID) subClass].optsInt[cast(ID) symbol]) = val; | |
120 subClassChanges[cast(ID) subClass].setInt (cast(ID) symbol, val); | |
121 } catch (ArrayBoundsException) { | |
122 // log and ignore: | |
123 logger.error (ERR_MSG); | |
124 } | |
125 } | |
126 static void setCharA (char[] subClass, char[] symbol, char[] val) { | |
127 changed = true; // something got set (don't bother checking this isn't what it already was) | |
128 | |
129 try { | |
130 *(subClasses[cast(ID) subClass].optsCharA[cast(ID) symbol]) = val; | |
131 subClassChanges[cast(ID) subClass].setCharA (cast(ID) symbol, val); | |
132 } catch (ArrayBoundsException) { | |
133 // log and ignore: | |
134 logger.error (ERR_MSG); | |
135 } | |
136 } | |
137 | |
138 // Track all sections for saving/loading/other generic handling. | |
139 static Options[ID] subClasses; | |
140 static OptionsGeneric[ID] subClassChanges; | |
141 static bool changed = false; // any changes at all, i.e. do we need to save? | |
142 | |
143 /* Load/save options from file. | |
144 * | |
145 * If the file doesn't exist, no reading is attempted (options are left at default values). | |
146 */ | |
147 private static const fileName = "options"; | |
148 private static const MT_LOAD_EXC = "Loading options aborted:"; | |
149 static void load () { | |
150 // Check it exists (if not it should still be created on exit). | |
151 // Don't bother checking it's not a folder, because it could still be a block or something. | |
152 if (!confDir.exists (fileName)) return; | |
153 | |
154 try { | |
155 IReader reader; | |
156 reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH); | |
157 reader.dataSecCreator = delegate IDataSection(ID id) { | |
158 /* Recognise each defined section, and return null for unrecognised sections. */ | |
159 Options* p = id in subClasses; | |
160 if (p !is null) return *p; | |
161 else return null; | |
162 }; | |
163 reader.read; | |
164 } catch (MTException e) { | |
165 logger.fatal (MT_LOAD_EXC); | |
166 logger.fatal (e.msg); | |
167 throw new optionsLoadException ("Mergetag exception (see above message)"); | |
168 } | |
169 } | |
170 static void save () { | |
171 if (!changed) return; // no changes to save | |
172 | |
173 DataSet ds = new DataSet(); | |
174 foreach (id, sec; subClassChanges) ds.sec[id] = sec; | |
175 | |
176 // Read locally-stored options | |
177 try { | |
178 IReader reader; | |
179 reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds); | |
180 reader.dataSecCreator = delegate IDataSection(ID id) { | |
181 return null; // All recognised sections are already in the dataset. | |
182 }; | |
183 reader.read; | |
184 } catch (MTFileIOException) { | |
185 // File either didn't exist or couldn't be opened. | |
186 // Presuming the former, this is not a problem. | |
187 } catch (MTException e) { | |
188 // Log a message and continue, overwriting the file: | |
189 logger.error (MT_LOAD_EXC); | |
190 logger.error (e.msg); | |
191 } | |
192 | |
193 try { | |
194 IWriter writer; | |
195 writer = confDir.makeMTWriter (fileName, ds); | |
196 writer.write(); | |
197 } catch (MTException e) { | |
198 logger.error ("Saving options aborted! Reason:"); | |
199 logger.error (e.msg); | |
200 } | |
201 } | |
202 | |
203 private static Logger logger; | |
204 static this() { | |
205 logger = Log.getLogger ("mde.options"); | |
206 } | |
207 //END Static | |
208 | |
209 //BEGIN Templates | |
210 template store(A...) { | |
211 alias A a; | |
212 } | |
213 template decRecurse(char[] A, B...) { | |
214 static if (B.length) const char[] decRecurse = A ~ ", " ~ decRecurse!(B); | |
215 else const char[] decRecurse = A; | |
216 } | |
217 template decBool(A...) { | |
218 const char[] decBool = "bool " ~ decRecurse!(A) ~ ";\n"; | |
219 } | |
220 template decCharA(A...) { | |
221 const char[] decCharA = "char[] " ~ decRecurse!(A) ~ ";\n"; | |
222 } | |
223 template decInt(A...) { | |
224 const char[] decInt = "int " ~ decRecurse!(A) ~ ";\n"; | |
225 } | |
226 template aaRecurse(char[] A, B...) { | |
227 static if (B.length) const char[] aaRecurse = "\""~A~"\"[]:&"~A ~ ", " ~ aaRecurse!(B); | |
228 else const char[] aaRecurse = "\""~A~"\"[]:&"~A; | |
229 } | |
230 template aaBool(A...) { | |
231 const char[] aaBool = "optsBool = [" ~ aaRecurse!(A) ~ "];\n"; | |
232 } | |
233 template aaInt(A...) { | |
234 const char[] aaInt = "optsInt = [" ~ aaRecurse!(A) ~ "];\n"; | |
235 } | |
236 template aaCharA(A...) { | |
237 const char[] aaCharA = "optsCharA = [" ~ aaRecurse!(A) ~ "];\n"; | |
238 } | |
239 //END Templates | |
240 } | |
241 | |
242 /* Special class to store all locally changed options, whatever the section. */ | |
243 class OptionsGeneric : Options { | |
244 // These store the actual values, but are never accessed directly except when initially added. | |
245 // optsX store pointers to each item added along with the ID and are used for access. | |
246 bool[] bools; | |
247 int[] ints; | |
248 char[][] strings; | |
249 | |
250 this () {} | |
251 | |
252 void setBool (ID id, bool x) { | |
253 bool** p = id in optsBool; | |
254 if (p !is null) **p = x; | |
255 else { | |
256 bools ~= x; | |
257 optsBool[id] = &bools[$-1]; | |
258 } | |
259 } | |
260 void setInt (ID id, int x) { | |
261 int** p = id in optsInt; | |
262 if (p !is null) **p = x; | |
263 else { | |
264 ints ~= x; | |
265 optsInt[id] = &ints[$-1]; | |
266 } | |
267 } | |
268 void setCharA (ID id, char[] x) { | |
269 char[]** p = id in optsCharA; | |
270 if (p !is null) **p = x; | |
271 else { | |
272 strings ~= x; | |
273 optsCharA[id] = &strings[$-1]; | |
274 } | |
275 } | |
276 | |
277 //BEGIN Mergetag loading/saving code | |
278 // Reverse priority: only load symbols not currently existing | |
279 void addTag (char[] tp, ID id, char[] dt) { | |
280 if (tp == "bool") { | |
281 if ((id in optsBool) is null) { | |
282 bools ~= parseTo!(bool) (dt); | |
283 optsBool[id] = &bools[$-1]; | |
284 } | |
285 } else if (tp == "char[]") { | |
286 if ((id in optsCharA) is null) { | |
287 strings ~= parseTo!(char[]) (dt); | |
288 optsCharA[id] = &strings[$-1]; | |
289 } | |
290 char[]** p = id in optsCharA; | |
291 if (p !is null) **p = parseTo!(char[]) (dt); | |
292 } else if (tp == "int") { | |
293 if ((id in optsInt) is null) { | |
294 ints ~= parseTo!(int) (dt); | |
295 optsInt[id] = &ints[$-1]; | |
296 } | |
297 } | |
298 } | |
299 //END Mergetag loading/saving code | |
300 } | |
301 | |
302 /* NOTE: These Options classes use templates to ease inserting contents. | |
303 * | |
304 * Each entry has an I18nTranslation entry; see data/L10n/ClassName.mtt for descriptions. | |
305 * | |
306 * To create a new class, just copy and paste anywhere and adjust. | |
307 */ | |
308 | |
309 /** A home for all miscellaneous options, at least for now. */ | |
310 OptionsMisc miscOpts; | |
311 class OptionsMisc : Options { | |
312 alias store!("useThreads") BOOL; | |
313 alias store!("logLevel") INT; | |
314 alias store!("L10n") CHARA; | |
315 | |
316 mixin (decBool!(BOOL.a)); | |
317 mixin (decInt!(INT.a)); | |
318 mixin (decCharA!(CHARA.a)); | |
319 | |
320 this () { | |
321 mixin (aaBool!(BOOL.a)); | |
322 mixin (aaInt!(INT.a)); | |
323 mixin (aaCharA!(CHARA.a)); | |
324 } | |
325 | |
326 static this() { | |
327 miscOpts = new OptionsMisc; | |
328 Options.addOptionsClass (miscOpts, "misc"); | |
329 } | |
330 } |