comparison mde/gui/widget/WidgetManager.d @ 175:1cbde9807293

Compile/link-time fixes for ldc & non-debug builds. Moved WidgetManager to widget/ Reverted IChildWidget to an interface, not an abstract class. Introduced a work-around for a compiler problem. May not cover all cases.
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 11 Sep 2009 20:56:53 +0200
parents mde/gui/WidgetManager.d@a1ba9157510e
children af40e9679436
comparison
equal deleted inserted replaced
174:3d58adc17d20 175:1cbde9807293
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 * The widget manager; root of the widget tree.
18 *
19 * Rendering is handled separately by an IRenderer.
20 *****************************************************************************/
21 module mde.gui.widget.WidgetManager;
22
23 import mde.gui.WidgetDataSet;
24 import mde.gui.widget.Ifaces;
25 import mde.gui.renderer.createRenderer;
26
27 import imde = mde.imde;
28 import mde.content.Content;
29 import mde.content.ServiceContent;
30 debug import mde.content.miscContent; // Debug menu
31 debug import mde.content.Debug;
32
33 // Widgets to create:
34 import mde.gui.widget.layout;
35 import mde.gui.widget.miscWidgets;
36 import mde.gui.widget.TextWidget;
37 import mde.gui.widget.contentFunctions;
38 import mde.gui.widget.miscContent;
39 import mde.gui.widget.Floating;
40 import mde.gui.widget.ParentContent;
41 import mde.gui.widget.AParentWidget;
42
43 public import tango.core.sync.Mutex;
44 import tango.util.log.Log : Log, Logger;
45 import tango.io.Console; // to print exception stack-trace
46 import tango.util.container.SortedMap;
47
48 private Logger logger;
49 static this () {
50 logger = Log.getLogger ("mde.gui.WidgetManager");
51 }
52
53 /******************************************************************************
54 * Methods in this class are only intended for use within the gui package,
55 * either by widgets (the IXXXWidget methods implementing from an interface in
56 * widgets.Ifaces.d) or by a derived class (back-end methods doing widget
57 * work). None of these methods are intended to be thread-safe when called
58 * concurrently on the same WidgetManager instance, but they should be thread-
59 * safe for calling on separate instances.
60 *****************************************************************************/
61 abstract class AWidgetManager : IWidgetManager
62 {
63 //BEGIN Public methods, for use outside the widget package
64 /** Construct a new widget manager.
65 *
66 * Params:
67 * name = The file name of the config for this GUI (to identify multiple GUIs). */
68 this (char[] name) {
69 auto p = "MiscOptions.l10n" in Content.allContent;
70 assert (p, "MiscOptions.l10n not created!");
71 p.addCallback (&reloadStrings);
72
73 serviceContent = ServiceContentList.createItems (name);
74 assert (cast (IServiceContent) Content.get ("menus.services."~name));
75
76 debug { // add a debug-mode menu
77 auto lWS = new EventContent ("menus.debug."~name~".logWidgetSize");
78 lWS.addCallback (&logWidgetSize);
79 }
80 }
81
82 /** A change callback on MiscOptions.l10n content to update widgets.
83 *
84 * Relies on another callback reloading translations to content first! */
85 final void reloadStrings (IContent) {
86 synchronized(mutex) {
87 if (childRoot is null) return;
88 childRoot.setup (++setupN, 2);
89 childRoot.setWidth (w, -1);
90 childRoot.setHeight (h, -1);
91 childRoot.setPosition (0,0);
92 childContext.setup (setupN, 2);
93 //TODO: possibly childDragged?
94 requestRedraw;
95 }
96 }
97
98 debug public void logWidgetSize (IContent) {
99 logger.trace ("size: {,4},{,4}; minimal: {,4},{,4} - WidgetManager", w,h, mw,mh);
100 logger.trace ("childRoot:");
101 childRoot.logWidgetSize;
102 logger.trace ("childContext:");
103 childContext.logWidgetSize;
104 if (childDragged !is null) {
105 logger.trace ("childDragged:");
106 childDragged.logWidgetSize;
107 }
108 }
109
110
111 //BEGIN Public IWidget methods
112 override bool saveChanges () {
113 bool ret = childRoot.saveChanges;
114 ret |= childContext.saveChanges;
115 if (childDragged !is null)
116 ret |= childDragged.saveChanges;
117 return ret;
118 }
119
120 /** Draw all widgets */
121 override void draw () {
122 if (childRoot)
123 childRoot.draw;
124 drawPopup;
125 }
126 //END Public IWidget methods
127 //END Public methods, for use outside the widget package
128
129 //BEGIN IWidget methods for widgets
130 public override bool dropContent (IContent content) {
131 return false;
132 }
133 //END IWidget methods for widgets
134
135 //BEGIN IParentWidget methods
136 // If call reaches the widget manager there isn't any recursion.
137 //NOTE: should be override
138 final void recursionCheck (widgetID, IContent) {}
139
140 override void minWChange (IChildWidget widget, wdim nmw) {
141 if (widget !is childRoot) { // Probably because widget is a popup widget
142 // This may get called from a CTOR, hence we can't check widget is one of childContext, etc.
143 if (widget.width < nmw)
144 widget.setWidth (nmw, -1);
145 return;
146 }
147 mw = nmw;
148 if (w < nmw) {
149 childRoot.setWidth (nmw, -1);
150 w = nmw;
151 }
152 childRoot.setPosition (0,0);
153 requestRedraw;
154 }
155 override void minHChange (IChildWidget widget, wdim nmh) {
156 if (widget !is childRoot) {
157 if (widget.height < nmh)
158 widget.setHeight (nmh, -1);
159 return;
160 }
161 mh = nmh;
162 if (h < nmh) {
163 childRoot.setHeight (nmh, -1);
164 h = nmh;
165 }
166 childRoot.setPosition (0,0);
167 requestRedraw;
168 }
169 //END IParentWidget methods
170
171 //BEGIN IPopupParentWidget methods
172 override IPopupParentWidget getParentIPPW () {
173 return this;
174 }
175
176 override void addChildIPPW (IPopupParentWidget ippw) {
177 requestRedraw;
178 if (ippw is childContext) { // special handling - a separate IPPW
179 contextActive = true;
180 return;
181 }
182 if (childIPPW)
183 childIPPW.removedIPPW;
184 childIPPW = ippw;
185 }
186 override bool removeChildIPPW (IPopupParentWidget ippw) {
187 if (ippw is childContext && contextActive) {
188 childContext.removedIPPW;
189 contextActive = false;
190 return true;
191 }
192 if (childIPPW !is ippw) return false;
193 childIPPW.removedIPPW;
194 childIPPW = null;
195 mAIPPW = MenuPosition.INACTIVE;
196 requestRedraw;
197 return true;
198 }
199
200 override void menuActive (MenuPosition mA) {
201 mAIPPW = mA;
202 if (childIPPW)
203 childIPPW.menuActive = mA;
204 if (contextActive)
205 childContext.menuActive = mA;
206 }
207 override MenuPosition menuActive () {
208 return mAIPPW;
209 }
210 override MenuPosition parentMenuActive () {
211 return MenuPosition.INACTIVE;
212 }
213
214 // Note: also triggered by non-popup widgets
215 override void menuDone () {}
216
217 override IChildWidget getPopupWidget (wdabs cx, wdabs cy, bool closePopup) {
218 IChildWidget ret;
219 // Don't bother with childDragged; it has no interaction
220 if (contextActive) {
221 ret = childContext.getPopupWidget (cx, cy, closePopup);
222 if (ret) return ret;
223 if (closePopup) {
224 childContext.removedIPPW;
225 contextActive = false;
226 requestRedraw;
227 }
228 }
229 if (childIPPW) {
230 ret = childIPPW.getPopupWidget (cx, cy, closePopup);
231 if (ret) return ret;
232 if (closePopup) {
233 removeChildIPPW (childIPPW);
234 }
235 }
236 return null;
237 }
238
239 override void drawPopup () {
240 if (childIPPW)
241 childIPPW.drawPopup;
242 if (contextActive)
243 childContext.drawPopup();
244 if (childDragged)
245 childDragged.draw();
246 }
247
248 debug override bool isChild (IPopupParentWidget ippw) {
249 if (contextActive && ippw is childContext)
250 return true;
251 return ippw is childIPPW;
252 }
253
254 override void removedIPPW () {} // irrelevant
255 //END IPopupParentWidget methods
256
257 //BEGIN IWidgetManager methods
258 override IChildWidget makeWidget (IParentWidget parent, widgetID id, IContent content = null)
259 {
260 debug assert (parent, "makeWidget: parent is null (code error)");
261 debug scope (failure)
262 logger.warn ("Creating widget \""~id~"\" failed.");
263
264 WidgetData data = curData[id];
265 if (data.ints.length < 1) {
266 logger.error ("No int data; creating a debug widget");
267 data.ints = [WIDGET_TYPE.Debug];
268 }
269 int type = data.ints[0]; // type is first element of data
270
271 try {
272 // Statically programmed binary search on type, returning a new widget or calling a
273 // function:
274 //pragma (msg, binarySearch ("type", WIDGETS));
275 mixin (binarySearch ("type", WIDGETS));
276 // Not returned a new widget:
277 logger.error ("Bad widget type: {}; creating a debug widget instead",type);
278 } catch (Exception e) {
279 logger.error ("Error creating widget; creating a debug widget instead. Exception printed to stderr.");
280 //TODO: find a standard way to output exceptions, and implement everywhere:
281 e.writeOut(delegate void(char[]s){ Cerr(s); });
282 }
283
284 return new DebugWidget (this, parent, id, data, content);
285 }
286
287 override WidgetData widgetData (widgetID id) {
288 return curData[id];
289 }
290 override void widgetData (widgetID id, WidgetData d) {
291 changes[id] = d; // also updates WidgetDataSet in data.
292 }
293
294 override wdims dimData (widgetID id) {
295 return curData.dims (id);
296 }
297 override void dimData (widgetID id, wdims d) {
298 changes.setDims(id, d); // also updates WidgetDataSet in data.
299 }
300
301 IRenderer renderer () {
302 assert (rend !is null, "WidgetManager.renderer: rend is null");
303 return rend;
304 }
305
306 MenuPosition positionPopup (IChildWidget parent, IChildWidget popup, MenuPosition position = MenuPosition.INACTIVE) {
307 debug assert (parent && popup, "positionPopup: null widget");
308 debug if (Debug.logPopupPositioning())
309 logger.trace ("Placing popup {} in relation to parent {}; input position: {}", popup, parent, position);
310 wdim w = popup.width,
311 h = popup.height,
312 x, y;
313 if (position & MenuPosition.ACTIVE) {
314 y = parent.yPos; // height flush with top
315 if (y+h > this.h) y += parent.height - h; // or bottom
316 if (position & MenuPosition.LEFT) { // previously left
317 x = parent.xPos - w; // on left
318 if (x < 0) {
319 x = parent.xPos + parent.width; // on right
320 position = MenuPosition.RIGHT;
321 }
322 } else { // previously right or above/below
323 x = parent.xPos + parent.width; // on right
324 position = MenuPosition.RIGHT;
325 if (x+w > this.w) {
326 x = parent.xPos - w; // or left
327 position = MenuPosition.LEFT;
328 }
329 }
330 } else {
331 wdim pw = parent.width;
332 if (popup.minWidth <= pw)
333 popup.setWidth (pw, -1); // neatness
334 x = parent.xPos; // align on left edge
335 if (x+w > this.w) x += pw - w; // align on right edge
336 y = parent.yPos + parent.height; // place below
337 if (y+h > this.h) y = parent.yPos - h; // or above
338 position = MenuPosition.ACTIVE;
339 }
340 if (x < 0) x = 0; // may be placed partially off-screen
341 if (y < 0) y = 0;
342 popup.setPosition (x, y);
343 debug if (Debug.logPopupPositioning())
344 logger.trace ("Placed popup {} of size ({},{}) at ({},{}); output position: {}", popup, w,h, x,y, position);
345 return position;
346 }
347
348 void requestRedraw () {
349 imde.mainSchedule.request(imde.SCHEDULE.DRAW);
350 }
351 //END IWidgetManager methods
352
353 protected:
354 // These methods are called by derived classes to do the widget-management work
355 //BEGIN WidgetManagement methods
356 /** Second stage of widget loading.
357 *
358 * Widget data should be loaded before this is called. */
359 final void createWidgets () {
360 // The renderer needs to be created on the first load, but not after this.
361 if (rend is null)
362 rend = createRenderer (rendName);
363
364 debug (mdeWidgets) logger.trace ("Creating root widget...");
365 childRoot = makeWidget (this, "root");
366 underMouse = childRoot; // don't leave null due to a check
367 debug (mdeWidgets) logger.trace ("Setting up root widget...");
368 childRoot.setup (0, 3);
369
370 mw = childRoot.minWidth;
371 mh = childRoot.minHeight;
372 matchMinimalSize ();
373
374 debug (mdeWidgets) logger.trace ("Setting size and position of root widget...");
375 childRoot.setWidth (w, -1);
376 childRoot.setHeight (h, -1);
377 childRoot.setPosition (0,0);
378 debug (mdeWidgets) logger.trace ("Done creating root widget.");
379
380 childContext = new PopupHandlerWidget (this, this, "contextHandler", "context", serviceContent);
381 childContext.setup (0,3);
382 debug (mdeWidgets) logger.trace ("Created context handler widget.");
383
384 underMouse = childRoot; // must be something
385 }
386
387 final void wmSizeEvent (int nw, int nh) {
388 w = cast(wdim) nw;
389 h = cast(wdim) nh;
390 matchMinimalSize;
391
392 if (!childRoot) return; // if not created yet.
393 childRoot.setWidth (w, -1);
394 childRoot.setHeight (h, -1);
395 childRoot.setPosition (0,0);
396 debug logWidgetSize (null);
397 }
398
399 /** For mouse click events.
400 *
401 * Sends the event on to the relevant windows and all click callbacks. */
402 final void wmMouseClick (wdabs cx, wdabs cy, ubyte b, bool state) {
403 if (childRoot is null) return;
404
405 // Update underMouse to get the widget clicked on
406 updateUnderMouse (cx, cy, state);
407
408 // end of a drag?
409 if (dragStart !is null && b == dragButton && state == false) {
410 IChildWidget dS = dragStart;
411 dragStart = null;
412 childDragged = null;
413 requestRedraw;
414 if (dS.dragRelease (cx, cy, underMouse))
415 return;
416 }
417
418 // Disable keyboard input if on another widget:
419 if (keyFocus && keyFocus !is underMouse) {
420 keyFocus.keyFocusLost;
421 keyFocus = null;
422 setLetterCallback (null);
423 }
424
425 // Finally, post the actual event:
426 if (b == 3 && state) { // right click - open context menu
427 Content contextContent = cast(Content) underMouse.content;
428 if (contextContent !is null) {
429 serviceContent.setContent (contextContent);
430 childContext.openMenu (underMouse, contextContent);
431 }
432 } else { // post other button presses to clickEvent
433 int ret = underMouse.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state);
434 if (ret & 1) { // keyboard input requested
435 keyFocus = underMouse;
436 setLetterCallback (&underMouse.keyEvent);
437 }
438 if (ret & 2 && dragStart is null) { // drag events requested
439 dragStart = underMouse;
440 dragButton = b; // currently we allow any button to be used for a drag, but.. ?
441 if (ret & 4) {
442 IContent c = underMouse.content();
443 if (c) { // NOTE: creates a new widget, not optimal
444 childDragged = new DisplayContentWidget (this, this, "dragContentDisplay", WidgetData ([0], []), c);
445 childDragged.setup (0, 3);
446 dragX = underMouse.xPos - cx;
447 dragY = underMouse.yPos - cy;
448 childDragged.setPosition (cx + dragX, cy + dragY);
449 }
450 }
451 }
452 }
453 }
454
455 /** For mouse motion events.
456 *
457 * Lock on mutex before calling. Pass new mouse coordinates. */
458 final void wmMouseMotion (wdabs cx, wdabs cy) {
459 updateUnderMouse (cx, cy, false);
460
461 if (dragStart !is null) {
462 dragStart.dragMotion (cx, cy, underMouse);
463 if (childDragged !is null) {
464 childDragged.setPosition (cx + dragX, cy + dragY);
465 requestRedraw;
466 }
467 }
468 }
469
470 // for internal use
471 private final void updateUnderMouse (wdabs cx, wdabs cy, bool closePopup) {
472 auto oUM = underMouse;
473 underMouse = getPopupWidget (cx, cy, closePopup);
474 if (underMouse is null) {
475 debug assert (childRoot.onSelf (cx, cy), "WidgetManager: childRoot doesn't cover whole area");
476 underMouse = childRoot.getWidget (cx, cy);
477 }
478 debug assert (oUM && underMouse, "no widget under mouse: error");
479 if (underMouse !is oUM) {
480 oUM.underMouse (false);
481 underMouse.underMouse (true);
482 debug if (Debug.logUnderMouse())
483 logger.trace ("Widget under mouse: {}", underMouse);
484 }
485 }
486
487 /** If possible, the screen-interaction derived class should override to
488 * make sure the window is at least (mw,mh) in size. In any case, this
489 * method MUST make sure w >= mw and h >= mh even if the window isn't this
490 * big.
491 *
492 * A resize may not be required when this is called, however. */
493 void matchMinimalSize () {
494 if (w < mw) {
495 logger.warn ("Min width for gui, {}, not met: {}", mw, w);
496 w = mw;
497 }
498 if (h < mh) {
499 logger.warn ("Min height for gui, {}, not met: {}", mh, h);
500 h = mh;
501 }
502 }
503
504 /// This should be overloaded to set a callback receiving keyboard input.
505 abstract void setLetterCallback(void delegate(ushort, char[]));
506 //END WidgetManagement methods
507
508 //BEGIN makeWidget metacode
509 private static {
510 /// Widget types. Items match widget names without the "Widget" suffix.
511 enum WIDGET_TYPE : int {
512 FUNCTION = 0x2000, // Function called instead of widget created (no "Widget" appended to fct name)
513 TAKES_CONTENT = 0x4000, // Flag indicates widget's this should be passed an IContent reference.
514
515 // Use widget names rather than usual capitals convention
516 Unnamed = 0x0, // Only for use by widgets not created with createWidget
517
518 // blank: 0x1
519 FixedBlank = 0x1,
520 SizableBlank = 0x2,
521 Debug = TAKES_CONTENT | 0xF,
522
523 // popup widgets: 0x10
524 PopupMenu = TAKES_CONTENT | 0x11,
525
526 // labels: 0x20
527 TextLabel = 0x21,
528
529 // content functions: 0x30
530 editContent = FUNCTION | TAKES_CONTENT | 0x30,
531 addContent = FUNCTION | 0x31,
532 popupListContent = FUNCTION | TAKES_CONTENT | 0x33,
533
534 // content widgets: 0x40
535 DisplayContent = TAKES_CONTENT | 0x40,
536 BoolContent = TAKES_CONTENT | 0x41,
537 AStringContent = TAKES_CONTENT | 0x42,
538 ButtonContent = TAKES_CONTENT | 0x43,
539 SliderContent = TAKES_CONTENT | 0x44,
540
541 GridLayout = TAKES_CONTENT | 0x100,
542 ContentList = TAKES_CONTENT | 0x110,
543
544 FloatingArea = TAKES_CONTENT | 0x200,
545 Border = TAKES_CONTENT | 0x204,
546 Switch = TAKES_CONTENT | 0x210,
547 Collapsible = TAKES_CONTENT | 0x214,
548 }
549
550 // Only used for binarySearch algorithm generation; must be ordered by numerical values.
551 const char[][] WIDGETS = [
552 "FixedBlank",
553 "SizableBlank",
554 "TextLabel",
555 "addContent",
556 "Debug",
557 "PopupMenu",
558 "DisplayContent",
559 "BoolContent",
560 "AStringContent",
561 "ButtonContent",
562 "SliderContent",
563 "GridLayout",
564 "ContentList",
565 "FloatingArea",
566 "Border",
567 "Switch",
568 "Collapsible",
569 "editContent",
570 "popupListContent"];
571
572 /* Generates a binary search algorithm for makeWidget. */
573 char[] binarySearch (char[] var, char[][] consts) {
574 if (consts.length > 3) {
575 return `if (`~var~` <= WIDGET_TYPE.`~consts[$/2 - 1]~`) {` ~
576 binarySearch (var, consts[0 .. $/2]) ~
577 `} else {` ~
578 binarySearch (var, consts[$/2 .. $]) ~
579 `}`;
580 } else {
581 char[] ret;
582 foreach (c; consts) {
583 ret ~= `if (` ~ var ~ ` == WIDGET_TYPE.` ~ c ~ `) {
584 debug (mdeWidgets) logger.trace ("Creating new `~c~`.");
585 parent.recursionCheck (id, content);
586 static if (WIDGET_TYPE.`~c~` & WIDGET_TYPE.FUNCTION)
587 return `~c~` (this, parent, id, data, content);
588 else static if (WIDGET_TYPE.`~c~` & WIDGET_TYPE.TAKES_CONTENT)
589 return new `~c~`Widget (this, parent, id, data, content);
590 else
591 return new `~c~`Widget (this, parent, id, data);
592 } else `;
593 }
594 ret = ret[0..$-6]; // remove last else
595 return ret;
596 }
597 }
598
599 debug { // check items in WIDGETS are listed in order
600 char[] WIDGETS_check () {
601 char[] ret;
602 for (int i = WIDGETS.length-2; i > 0; --i) {
603 ret ~= "WIDGET_TYPE."~WIDGETS[i] ~" >= WIDGET_TYPE."~ WIDGETS[i+1];
604 if (i>1) ret ~= " || ";
605 }
606 return ret;
607 }
608 mixin ("static if ("~WIDGETS_check~")
609 static assert (false, \"WIDGETS is not in order!\");");
610 }
611 }
612 //END makeWidget metacode
613
614 protected:
615 // Main child widget:
616 IChildWidget childRoot; // Root of the main GUI widget tree
617
618 // Dimensions and child set-up data (fit to childRoot):
619 wdim w,h; // current widget size; should be at least (mw,mh) even if not displayable
620 wdim mw,mh; // minimal area required by widgets
621 uint setupN; // n to pass to IChildWidget.setup
622
623 // IPopupParentWidget stuff for childRoot:
624 MenuPosition mAIPPW; // IPPW variable
625 IPopupParentWidget childIPPW; // child IPPW, if any active
626
627 IChildWidget keyFocus; // widget receiving keyboard input
628 IChildWidget underMouse; // widget under the mouse pointer; should never be null when childRoot is non-null
629
630
631 // Context menu:
632 // Essentially, we consider childContext a full child IPPW, but handle it separately from
633 // childIPPW. Instead of providing another ref. for this IPPW, shortcut by using this reference
634 // and the boolean contextActive:
635 scope PopupHandlerWidget childContext; // context menu popup (handler)
636 bool contextActive = false; // If true, consider childContext a child IPPW
637 scope IServiceContent serviceContent; // context menu content tree
638
639
640 // Drag-and-drop data:
641 //NOTE: could be wrapped with a PopupHandlerWidget, but can't set position then?
642 scope IChildWidget childDragged; // displays dragged content; no interaction
643 IChildWidget dragStart; // if non-null, this widget should receive motion and click-release events
644 int dragButton; // index of button in use for drag
645 wdrel dragX, dragY; // coordinates of dragged content relative to mouse
646
647
648 // Renderer:
649 char[] rendName; // Name of renderer; for saving and creating renderers
650 scope IRenderer rend;
651
652
653 // Data loaded/to save:
654 WidgetDataSet curData; // Current data
655 WidgetDataChanges changes; // Changes for the current design.
656
657 Mutex mutex; // lock on methods for use outside the package.
658 }