Mercurial > projects > mde
comparison mde/gui/WidgetManager.d @ 159:b06b04c75e86
Finished last commit, rearranged code for the WidgetManager class.
There is now a GUI options section.
Created a third WidgetManager class called WidgetLoader to handle file loading/saving.
Moved most of the code in WMScreen's draw/clickEvent/motionEvent functions to WidgetManager.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Thu, 21 May 2009 20:55:10 +0200 |
parents | c67d074a7111 |
children | ccd01fde535e |
comparison
equal
deleted
inserted
replaced
158:f132e599043f | 159:b06b04c75e86 |
---|---|
24 *****************************************************************************/ | 24 *****************************************************************************/ |
25 module mde.gui.WidgetManager; | 25 module mde.gui.WidgetManager; |
26 | 26 |
27 import mde.gui.WidgetDataSet; | 27 import mde.gui.WidgetDataSet; |
28 import mde.gui.widget.Ifaces; | 28 import mde.gui.widget.Ifaces; |
29 import mde.gui.exception; | |
30 | 29 |
31 import imde = mde.imde; | 30 import imde = mde.imde; |
32 import mde.content.Content; | 31 import mde.content.Content; |
33 debug import mde.content.miscContent; // Debug menu | 32 debug import mde.content.miscContent; // Debug menu |
34 | |
35 import mt = mde.file.mergetag.DataSet; | |
36 import mde.file.mergetag.Reader; | |
37 import mde.file.mergetag.Writer; | |
38 import mde.file.paths; | |
39 | 33 |
40 // Widgets to create: | 34 // Widgets to create: |
41 import mde.gui.widget.layout; | 35 import mde.gui.widget.layout; |
42 import mde.gui.widget.miscWidgets; | 36 import mde.gui.widget.miscWidgets; |
43 import mde.gui.widget.TextWidget; | 37 import mde.gui.widget.TextWidget; |
44 import mde.gui.widget.contentFunctions; | 38 import mde.gui.widget.contentFunctions; |
45 import mde.gui.widget.miscContent; | 39 import mde.gui.widget.miscContent; |
46 import mde.gui.widget.Floating; | 40 import mde.gui.widget.Floating; |
47 import mde.gui.widget.ParentContent; | 41 import mde.gui.widget.ParentContent; |
48 | 42 |
49 import tango.core.sync.Mutex; | 43 public import tango.core.sync.Mutex; |
50 import tango.util.log.Log : Log, Logger; | 44 import tango.util.log.Log : Log, Logger; |
51 import tango.util.container.SortedMap; | 45 import tango.util.container.SortedMap; |
52 | 46 |
53 private Logger logger; | 47 private Logger logger; |
54 static this () { | 48 static this () { |
56 } | 50 } |
57 | 51 |
58 /****************************************************************************** | 52 /****************************************************************************** |
59 * Contains the code for loading and saving an entire gui (more than one may | 53 * Contains the code for loading and saving an entire gui (more than one may |
60 * exist), but not the code for drawing it or handling user input. | 54 * exist), but not the code for drawing it or handling user input. |
55 * | |
56 * Methods in this class are only intended for use within the gui package, | |
57 * either by widgets (the IXXXWidget methods implementing from an interface in | |
58 * widgets.Ifaces.d) or by a derived class (back-end methods doing widget | |
59 * work). None of these methods are intended to be thread-safe when called | |
60 * concurrently on the same WidgetManager instance, but they should be thread- | |
61 * safe for calling on separate instances. | |
61 * | 62 * |
62 * This abstract class exists solely for separating out some of the functionality. | 63 * This abstract class exists solely for separating out some of the functionality. |
63 *****************************************************************************/ | 64 *****************************************************************************/ |
64 abstract scope class AWidgetManager : IWidgetManager | 65 abstract scope class AWidgetManager : IWidgetManager |
65 { | 66 { |
66 /** Construct a new widget loader. | 67 /** Construct a new widget manager. |
67 * | 68 * |
68 * params: | 69 * Params: |
69 * fileName = Name of file specifying the gui, excluding path and extension. | 70 * name = The file name of the config for this GUI (to identify multiple GUIs). */ |
70 */ | 71 protected this (char[] name) { |
71 protected this (char[] file) { | |
72 mutex = new Mutex; // Used on functions intended to be called from outside the gui package. | |
73 fileName = file; | |
74 | |
75 clickCallbacks = new typeof(clickCallbacks); | 72 clickCallbacks = new typeof(clickCallbacks); |
76 motionCallbacks = new typeof(motionCallbacks); | 73 motionCallbacks = new typeof(motionCallbacks); |
77 | 74 |
78 auto p = "MiscOptions.l10n" in Content.allContent; | 75 auto p = "MiscOptions.l10n" in Content.allContent; |
79 assert (p, "MiscOptions.l10n not created!"); | 76 assert (p, "MiscOptions.l10n not created!"); |
80 p.addCallback (&reloadStrings); | 77 p.addCallback (&reloadStrings); |
81 debug { // add a debug-mode menu | 78 debug { // add a debug-mode menu |
82 auto lWS = new EventContent ("menus.debug."~file~".logWidgetSize"); | 79 auto lWS = new EventContent ("menus.debug."~name~".logWidgetSize"); |
83 lWS.addCallback (&logWidgetSize); | 80 lWS.addCallback (&logWidgetSize); |
84 } | 81 } |
85 } | 82 } |
86 | 83 |
87 /* Load the widgets' data from the file specified to the CTOR. | 84 public: |
88 * | |
89 * params: | |
90 * allDesigns = Load all sections | |
91 */ | |
92 private void loadData (bool allDesigns = false) { | |
93 if (allLoaded || (defaultDesign !is null && allDesigns == false)) | |
94 return; // test if already loaded | |
95 | |
96 // Set up a reader | |
97 scope IReader reader; | |
98 try { | |
99 reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_LOW, null, true); | |
100 | |
101 // Read from the HEADER: | |
102 // Get the renderer | |
103 char[]* p = "Renderer" in reader.dataset.header._charA; | |
104 if (p is null || *p is null) { | |
105 logger.warn ("No renderer specified: using \"Simple\""); | |
106 rendName = "Simple"; | |
107 } | |
108 else | |
109 rendName = *p; | |
110 | |
111 // Get which section to use | |
112 p = "Design" in reader.dataset.header._charA; | |
113 if (p is null || *p is null) { | |
114 logger.warn ("No gui design specified: trying \"Default\""); | |
115 defaultDesign = "Default"; | |
116 } | |
117 else | |
118 defaultDesign = *p; | |
119 | |
120 // Read the body: | |
121 // Load the chosen design | |
122 reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) { | |
123 WidgetDataSet* p = id in data; | |
124 if (p is null) { | |
125 data[id] = new WidgetDataSet; | |
126 return *(id in data); | |
127 } | |
128 return *p; | |
129 }; | |
130 | |
131 if (allDesigns) { | |
132 reader.read; | |
133 allLoaded = true; | |
134 } else | |
135 reader.read([defaultDesign]); | |
136 } catch (NoFileException) { | |
137 logger.error ("Unable to load GUI: no config file: "~fileName); | |
138 // just return: not a fatal error (so long as the game can run without a GUI!) | |
139 } catch (Exception e) { | |
140 logger.error ("Unable to load GUI: errors parsing config file ("~confDir.getFileName(fileName,PRIORITY.HIGH_LOW)~"):"); | |
141 logger.error (e.msg); | |
142 throw new GuiException ("Failure parsing config file"); | |
143 } | |
144 } | |
145 | |
146 /** Load the gui from some design. | |
147 * | |
148 * If a design was previously loaded, its changes are saved first. | |
149 * | |
150 * Params: | |
151 * name = Design to load. If null, the default will be loaded. | |
152 */ | |
153 void loadDesign (char[] name = null) { | |
154 if (changes !is null) // A design was previously loaded | |
155 save; // own lock | |
156 | |
157 mutex.lock; | |
158 scope(exit) mutex.unlock; | |
159 | |
160 // Load data (loadData tests if it's already loaded first): | |
161 if (name is null) { | |
162 loadData (false); | |
163 name = defaultDesign; | |
164 } else | |
165 loadData (true); | |
166 | |
167 | |
168 // Get data: | |
169 auto p = name in data; | |
170 while (p is null) { | |
171 if (name == defaultDesign) | |
172 throw new GuiException ("Unable to load [specified or] default design"); | |
173 name = defaultDesign; // try again with the default | |
174 p = name in data; | |
175 } | |
176 curData = *p; | |
177 | |
178 // Get/create a changes section: | |
179 if (changesDS is null) | |
180 changesDS = new mt.DataSet; | |
181 | |
182 mt.IDataSection* q = name in changesDS.sec; | |
183 if (!q || ((changes = cast(WidgetDataChanges) *q) is null)) { | |
184 changes = new WidgetDataChanges (curData); | |
185 changesDS.sec[name] = changes; | |
186 } | |
187 | |
188 // Create the widgets: | |
189 createRootWidget; | |
190 underMouse = child; // must be something | |
191 } | |
192 | |
193 /** Save changes, if any exist. | |
194 * | |
195 * Is run when the manager is destroyed, but could be run at other times too. */ | |
196 void save () { | |
197 preSave; | |
198 | |
199 mutex.lock; | |
200 scope(exit) mutex.unlock; | |
201 | |
202 // Make all widgets save any changed data: | |
203 child.saveChanges; | |
204 | |
205 if (changes.noChanges) | |
206 return; | |
207 | |
208 if (loadUserFile) { // merge entries from user file into current changes | |
209 try { | |
210 scope IReader reader = confDir.makeMTReader ( | |
211 fileName, PRIORITY.HIGH_ONLY, changesDS, true); | |
212 | |
213 // Create if necessary, only corresponding to existing designs read: | |
214 reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) { | |
215 WidgetDataSet* p = id in data; | |
216 if (p is null) | |
217 throw new Exception ("File has changed since it was loaded!"); | |
218 return new WidgetDataChanges (*p); | |
219 }; | |
220 | |
221 reader.read; | |
222 } catch (NoFileException) { | |
223 // No user file exists; not an error. | |
224 } catch (Exception e) { | |
225 logger.error ("Error reading "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" prior to saving:"); | |
226 logger.error (e.msg); | |
227 logger.error ("Overwriting the file."); | |
228 // Continue... | |
229 } | |
230 loadUserFile = false; // don't need to do it again | |
231 } | |
232 | |
233 try { // Save | |
234 IWriter writer; | |
235 writer = confDir.makeMTWriter (fileName, changesDS); | |
236 writer.write; | |
237 } catch (Exception e) { | |
238 logger.error ("Saving to "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" failed:"); | |
239 logger.error (e.msg); | |
240 // No point in throwing since it doesn't affect anything else. | |
241 } | |
242 } | |
243 | |
244 /** Get the names of all designs available. */ | |
245 char[][] designs() { | |
246 synchronized(mutex) { | |
247 loadData (true); | |
248 return data.keys; | |
249 } | |
250 } | |
251 | |
252 /** A change callback on MiscOptions.l10n content to update widgets. | |
253 * | |
254 * Relies on another callback reloading translations to content first! */ | |
255 protected void reloadStrings (Content) { | |
256 synchronized(mutex) { | |
257 if (child is null) return; | |
258 child.setup (++setupN, 2); | |
259 child.setWidth (w, -1); | |
260 child.setHeight (h, -1); | |
261 child.setPosition (0,0); | |
262 requestRedraw; | |
263 } | |
264 } | |
265 | |
266 // These methods are only intended for use within the gui package. | |
267 // They are not necessarily thread-safe: | |
268 | |
269 //BEGIN IParentWidget methods | 85 //BEGIN IParentWidget methods |
270 // If call reaches the widget manager there isn't any recursion. | 86 // If call reaches the widget manager there isn't any recursion. |
271 //NOTE: should be override | 87 //NOTE: should be override |
272 final void recursionCheck (widgetID, IContent) {} | 88 final void recursionCheck (widgetID, IContent) {} |
273 | 89 |
469 logger.trace ("size: {,4},{,4}; minimal: {,4},{,4} - WidgetManager", w,h, mw,mh); | 285 logger.trace ("size: {,4},{,4}; minimal: {,4},{,4} - WidgetManager", w,h, mw,mh); |
470 child.logWidgetSize; | 286 child.logWidgetSize; |
471 } | 287 } |
472 | 288 |
473 protected: | 289 protected: |
290 // These methods are called by derived classes to do the widget-management work | |
291 //BEGIN WidgetManagement methods | |
292 /** Draw all widgets */ | |
293 void wmDrawWidgets() { | |
294 if (child) | |
295 child.draw; | |
296 if (childIPPW) | |
297 childIPPW.drawPopup; | |
298 drawPopup; | |
299 } | |
300 | |
301 /** For mouse click events. | |
302 * | |
303 * Sends the event on to the relevant windows and all click callbacks. */ | |
304 void wmMouseClick (wdabs cx, wdabs cy, ubyte b, bool state) { | |
305 if (child is null) return; | |
306 | |
307 // Callbacks have the highest priority receiving events (e.g. a button release) | |
308 foreach (dg; clickCallbacks) | |
309 if (dg (cx, cy, b, state)) return; | |
310 | |
311 // Update underMouse to get the widget clicked on | |
312 updateUnderMouse (cx, cy, state); | |
313 | |
314 // Disable keyboard input if on another widget: | |
315 if (keyFocus && keyFocus !is underMouse) { | |
316 keyFocus.keyFocusLost; | |
317 keyFocus = null; | |
318 setLetterCallback (null); | |
319 } | |
320 | |
321 // Finally, post the actual event: | |
322 if (b == 3 && state) { // right click - open context menu | |
323 IContent contextContent = underMouse.content; | |
324 if (contextContent is null) return; | |
325 // NOTE: Creates new widgets every time; not optimal | |
326 popupContext = makeWidget (this, "context", contextContent); | |
327 popupContext.setup (0, 3); | |
328 positionPopup (underMouse, popupContext); | |
329 requestRedraw; | |
330 } else // post other button presses to clickEvent | |
331 if (underMouse.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state) & 1) { | |
332 // keyboard input requested | |
333 keyFocus = underMouse; | |
334 setLetterCallback (&underMouse.keyEvent); | |
335 } | |
336 } | |
337 | |
338 /** For mouse motion events. | |
339 * | |
340 * Lock on mutex before calling. Pass new mouse coordinates. */ | |
341 void wmMouseMotion (wdabs cx, wdabs cy) { | |
342 foreach (dg; motionCallbacks) | |
343 dg (cx, cy); | |
344 | |
345 updateUnderMouse (cx, cy, false); | |
346 } | |
347 | |
348 | |
349 /** A change callback on MiscOptions.l10n content to update widgets. | |
350 * | |
351 * Relies on another callback reloading translations to content first! */ | |
352 void reloadStrings (Content) { | |
353 synchronized(mutex) { | |
354 if (child is null) return; | |
355 child.setup (++setupN, 2); | |
356 child.setWidth (w, -1); | |
357 child.setHeight (h, -1); | |
358 child.setPosition (0,0); | |
359 requestRedraw; | |
360 } | |
361 } | |
362 // for internal use | |
474 void updateUnderMouse (wdabs cx, wdabs cy, bool closePopup) { | 363 void updateUnderMouse (wdabs cx, wdabs cy, bool closePopup) { |
475 auto oUM = underMouse; | 364 auto oUM = underMouse; |
476 underMouse = getPopupWidget (cx, cy, closePopup); | 365 underMouse = getPopupWidget (cx, cy, closePopup); |
477 if (underMouse is null) { | 366 if (underMouse is null) { |
478 debug assert (child.onSelf (cx, cy), "WidgetManager: child doesn't cover whole area"); | 367 debug assert (child.onSelf (cx, cy), "WidgetManager: child doesn't cover whole area"); |
483 oUM.underMouse (false); | 372 oUM.underMouse (false); |
484 underMouse.underMouse (true); | 373 underMouse.underMouse (true); |
485 } | 374 } |
486 } | 375 } |
487 | 376 |
488 /** Second stage of loading the widgets. | 377 /// This should be overloaded to set a callback receiving keyboard input. |
489 * | 378 abstract void setLetterCallback(void delegate(ushort, char[])); |
490 * loadDesign handles the data; this method needs to: | 379 //END WidgetManagement methods |
491 * --- | |
492 * // 1. Create the root widget: | |
493 * child = makeWidget ("root"); | |
494 * child.setup (0, 3); | |
495 * // 2. Set the size: | |
496 * child.setWidth (child.minWidth, 1); | |
497 * child.setHeight (child.minHeight, 1); | |
498 * // 3. Set the position (necessary part of initialization): | |
499 * child.setPosition (0,0); | |
500 * --- | |
501 */ | |
502 void createRootWidget(); | |
503 | |
504 /** Called before saving (usually when the GUI is about to be destroyed, although not | |
505 * necessarily). */ | |
506 void preSave (); | |
507 | 380 |
508 public: | 381 public: |
509 //BEGIN makeWidget metacode | 382 //BEGIN makeWidget metacode |
510 private static { | 383 private static { |
511 /// Widget types. Items match widget names without the "Widget" suffix. | 384 /// Widget types. Items match widget names without the "Widget" suffix. |
611 } | 484 } |
612 } | 485 } |
613 //END makeWidget metacode | 486 //END makeWidget metacode |
614 | 487 |
615 protected: | 488 protected: |
616 // Dataset/design data: | |
617 final char[] fileName; | |
618 char[] defaultDesign; // The design specified in the file header. | |
619 char[] rendName; // Name of renderer; for saving and creating renderers | |
620 | |
621 // Loaded data, indexed by design name. May not be loaded for all gui designs: | |
622 scope WidgetDataSet[char[]] data; | |
623 private bool allLoaded = false; // applies to data | |
624 WidgetDataSet curData; // Current data | 489 WidgetDataSet curData; // Current data |
625 WidgetDataChanges changes; // Changes for the current design. | 490 WidgetDataChanges changes; // Changes for the current design. |
626 scope mt.DataSet changesDS; // changes and sections from user file (used for saving) | 491 |
627 bool loadUserFile = true; // still need to load user file for saving? | 492 char[] rendName; // Name of renderer; for saving and creating renderers |
628 | |
629 IRenderer rend; | 493 IRenderer rend; |
630 | 494 |
631 // Widgets: | 495 // Widgets: |
632 wdim w,h; // current widget size; should be at least (mw,mh) even if not displayable | 496 wdim w,h; // current widget size; should be at least (mw,mh) even if not displayable |
633 wdim mw,mh; // minimal area required by widgets | 497 wdim mw,mh; // minimal area required by widgets |