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