]> git.mxchange.org Git - flightgear.git/blob - src/GUI/FGPUIDialog.cxx
toggle fullscreen: also adapt GUI plane when resizing
[flightgear.git] / src / GUI / FGPUIDialog.cxx
1 // dialog.cxx: implementation of an XML-configurable dialog box.
2
3 #ifdef HAVE_CONFIG_H
4 #  include "config.h"
5 #endif
6
7 #include <simgear/structure/SGBinding.hxx>
8 #include <simgear/props/props_io.hxx>
9
10 #include <Scripting/NasalSys.hxx>
11 #include <Main/fg_os.hxx>
12 #include <Main/globals.hxx>
13 #include <Main/fg_props.hxx>
14
15 #include "FGPUIDialog.hxx"
16 #include "new_gui.hxx"
17 #include "AirportList.hxx"
18 #include "property_list.hxx"
19 #include "layout.hxx"
20 #include "WaypointList.hxx"
21 #include "CanvasWidget.hxx"
22 #include "MapWidget.hxx"
23 #include "FGFontCache.hxx"
24 #include "FGColor.hxx"
25
26 enum format_type { f_INVALID, f_INT, f_LONG, f_FLOAT, f_DOUBLE, f_STRING };
27 static const int FORMAT_BUFSIZE = 255;
28 static const int RESIZE_MARGIN = 7;
29
30
31 /**
32  * Makes sure the format matches '%[ -+#]?\d*(\.\d*)?(l?[df]|s)', with
33  * only one number or string placeholder and otherwise arbitrary prefix
34  * and postfix containing only quoted percent signs (%%).
35  */
36 static format_type
37 validate_format(const char *f)
38 {
39     bool l = false;
40     format_type type;
41     for (; *f; f++) {
42         if (*f == '%') {
43             if (f[1] == '%')
44                 f++;
45             else
46                 break;
47         }
48     }
49     if (*f++ != '%')
50         return f_INVALID;
51     while (*f == ' ' || *f == '+' || *f == '-' || *f == '#' || *f == '0')
52         f++;
53     while (*f && isdigit(*f))
54         f++;
55     if (*f == '.') {
56         f++;
57         while (*f && isdigit(*f))
58             f++;
59     }
60
61     if (*f == 'l')
62         l = true, f++;
63
64     if (*f == 'd') {
65         type = l ? f_LONG : f_INT;
66     } else if (*f == 'f')
67         type = l ? f_DOUBLE : f_FLOAT;
68     else if (*f == 's') {
69         if (l)
70             return f_INVALID;
71         type = f_STRING;
72     } else
73         return f_INVALID;
74
75     for (++f; *f; f++) {
76         if (*f == '%') {
77             if (f[1] == '%')
78                 f++;
79             else
80                 return f_INVALID;
81         }
82     }
83     return type;
84 }
85
86
87 ////////////////////////////////////////////////////////////////////////
88 // Implementation of GUIInfo.
89 ////////////////////////////////////////////////////////////////////////
90
91 /**
92  * User data for a GUI object.
93  */
94 struct GUIInfo
95 {
96     GUIInfo(FGPUIDialog *d);
97     virtual ~GUIInfo();
98     void apply_format(SGPropertyNode *);
99
100     FGPUIDialog *dialog;
101     SGPropertyNode_ptr node;
102     vector <SGBinding *> bindings;
103     int key;
104     string label, legend, text, format;
105     format_type fmt_type;
106 };
107
108 GUIInfo::GUIInfo (FGPUIDialog *d) :
109     dialog(d),
110     key(-1),
111     fmt_type(f_INVALID)
112 {
113 }
114
115 GUIInfo::~GUIInfo ()
116 {
117     for (unsigned int i = 0; i < bindings.size(); i++) {
118         delete bindings[i];
119         bindings[i] = 0;
120     }
121 }
122
123 void GUIInfo::apply_format(SGPropertyNode *n)
124 {
125     char buf[FORMAT_BUFSIZE + 1];
126     if (fmt_type == f_INT)
127         snprintf(buf, FORMAT_BUFSIZE, format.c_str(), n->getIntValue());
128     else if (fmt_type == f_LONG)
129         snprintf(buf, FORMAT_BUFSIZE, format.c_str(), n->getLongValue());
130     else if (fmt_type == f_FLOAT)
131         snprintf(buf, FORMAT_BUFSIZE, format.c_str(), n->getFloatValue());
132     else if (fmt_type == f_DOUBLE)
133         snprintf(buf, FORMAT_BUFSIZE, format.c_str(), n->getDoubleValue());
134     else
135         snprintf(buf, FORMAT_BUFSIZE, format.c_str(), n->getStringValue());
136
137     buf[FORMAT_BUFSIZE] = '\0';
138     text = buf;
139 }
140
141
142 \f
143 /**
144  * Key handler.
145  */
146 int fgPopup::checkKey(int key, int updown)
147 {
148     if (updown == PU_UP || !isVisible() || !isActive() || window != puGetWindow())
149         return false;
150
151     puObject *input = getActiveInputField(this);
152     if (input)
153         return input->checkKey(key, updown);
154
155     puObject *object = getKeyObject(this, key);
156     if (!object)
157         return puPopup::checkKey(key, updown);
158
159     // invokeCallback() isn't enough; we need to simulate a mouse button press
160     object->checkHit(PU_LEFT_BUTTON, PU_DOWN,
161             (object->getABox()->min[0] + object->getABox()->max[0]) / 2,
162             (object->getABox()->min[1] + object->getABox()->max[1]) / 2);
163     object->checkHit(PU_LEFT_BUTTON, PU_UP,
164             (object->getABox()->min[0] + object->getABox()->max[0]) / 2,
165             (object->getABox()->min[1] + object->getABox()->max[1]) / 2);
166     return true;
167 }
168
169 puObject *fgPopup::getKeyObject(puObject *object, int key)
170 {
171     puObject *ret;
172     if (object->getType() & PUCLASS_GROUP)
173         for (puObject *obj = ((puGroup *)object)->getFirstChild();
174                 obj; obj = obj->getNextObject())
175             if ((ret = getKeyObject(obj, key)))
176                 return ret;
177
178     GUIInfo *info = (GUIInfo *)object->getUserData();
179     if (info && info->key == key)
180         return object;
181
182     return 0;
183 }
184
185 puObject *fgPopup::getActiveInputField(puObject *object)
186 {
187     puObject *ret;
188     if (object->getType() & PUCLASS_GROUP)
189         for (puObject *obj = ((puGroup *)object)->getFirstChild();
190                 obj; obj = obj->getNextObject())
191             if ((ret = getActiveInputField(obj)))
192                 return ret;
193
194     if (object->getType() & (PUCLASS_INPUT|PUCLASS_LARGEINPUT)
195             && ((puInput *)object)->isAcceptingInput())
196         return object;
197
198     return 0;
199 }
200
201 /**
202  * Mouse handler.
203  */
204 int fgPopup::checkHit(int button, int updown, int x, int y)
205 {
206     int result = 0;
207     if (updown != PU_DRAG && !_dragging)
208         result = puPopup::checkHit(button, updown, x, y);
209
210     if (!_draggable)
211        return result;
212
213     // This is annoying.  We would really want a true result from the
214     // superclass to indicate "handled by child object", but all it
215     // tells us is that the pointer is inside the dialog.  So do the
216     // intersection test (again) to make sure we don't start a drag
217     // when inside controls.
218
219     if (updown == PU_DOWN && !_dragging) {
220         if (!result)
221             return 0;
222         int global_drag = fgGetKeyModifiers() & KEYMOD_SHIFT;
223         int global_resize = fgGetKeyModifiers() & KEYMOD_CTRL;
224
225         int hit = getHitObjects(this, x, y);
226         if (hit & PUCLASS_LIST)  // ctrl-click in property browser (toggle bool)
227             return result;
228         if (!global_resize && hit & (PUCLASS_BUTTON|PUCLASS_ONESHOT|PUCLASS_INPUT
229                 |PUCLASS_LARGEINPUT|PUCLASS_SCROLLBAR))
230             return result;
231
232         getPosition(&_dlgX, &_dlgY);
233         getSize(&_dlgW, &_dlgH);
234         _start_cursor = fgGetMouseCursor();
235         _dragging = true;
236         _startX = x;
237         _startY = y;
238
239         // check and prepare for resizing
240         static const int cursor[] = {
241             MOUSE_CURSOR_POINTER, MOUSE_CURSOR_LEFTSIDE, MOUSE_CURSOR_RIGHTSIDE, 0,
242             MOUSE_CURSOR_TOPSIDE, MOUSE_CURSOR_TOPLEFT, MOUSE_CURSOR_TOPRIGHT, 0,
243             MOUSE_CURSOR_BOTTOMSIDE, MOUSE_CURSOR_BOTTOMLEFT, MOUSE_CURSOR_BOTTOMRIGHT, 0,
244         };
245
246         _resizing = 0;
247         if (!global_drag && _resizable) {
248             int hmargin = global_resize ? _dlgW / 3 : RESIZE_MARGIN;
249             int vmargin = global_resize ? _dlgH / 3 : RESIZE_MARGIN;
250
251             if (y - _dlgY < vmargin)
252                 _resizing |= BOTTOM;
253             else if (_dlgY + _dlgH - y < vmargin)
254                 _resizing |= TOP;
255
256             if (x - _dlgX < hmargin)
257                 _resizing |= LEFT;
258             else if (_dlgX + _dlgW - x < hmargin)
259                 _resizing |= RIGHT;
260
261             if (!_resizing && global_resize)
262                 _resizing = BOTTOM|RIGHT;
263
264             _cursor = cursor[_resizing];
265            if (_resizing && _resizable)
266                 fgSetMouseCursor(_cursor);
267        }
268
269     } else if (updown == PU_DRAG && _dragging) {
270         if (_resizing) {
271             GUIInfo *info = (GUIInfo *)getUserData();
272             if (_resizable && info && info->node) {
273                 int w = _dlgW;
274                 int h = _dlgH;
275                 if (_resizing & LEFT)
276                     w += _startX - x;
277                 if (_resizing & RIGHT)
278                     w += x - _startX;
279                 if (_resizing & TOP)
280                     h += y - _startY;
281                 if (_resizing & BOTTOM)
282                     h += _startY - y;
283
284                 int prefw, prefh;
285                 LayoutWidget wid(info->node);
286                 wid.calcPrefSize(&prefw, &prefh);
287                 if (w < prefw)
288                     w = prefw;
289                 if (h < prefh)
290                     h = prefh;
291
292                 int x = _dlgX;
293                 int y = _dlgY;
294                 if (_resizing & LEFT)
295                     x += _dlgW - w;
296                 if (_resizing & BOTTOM)
297                     y += _dlgH - h;
298
299                 wid.layout(x, y, w, h);
300                 setSize(w, h);
301                 setPosition(x, y);
302                 applySize(static_cast<puObject *>(this));
303                 getFirstChild()->setSize(w, h); // dialog background puFrame
304             }
305         } else {
306             int posX = x + _dlgX - _startX,
307               posY = y + _dlgY - _startY;
308             setPosition(posX, posY);
309             
310             GUIInfo *info = (GUIInfo *)getUserData();
311             if (info && info->node) {
312                 info->node->setIntValue("x", posX);
313                 info->node->setIntValue("y", posY);
314             }
315         } // re-positioning
316
317     } else if (_dragging) {
318         fgSetMouseCursor(_start_cursor);
319         _dragging = false;
320     }
321     return result;
322 }
323
324 int fgPopup::getHitObjects(puObject *object, int x, int y)
325 {
326     if (!object->isVisible())
327         return 0;
328
329     int type = 0;
330     if (object->getType() & PUCLASS_GROUP)
331         for (puObject *obj = ((puGroup *)object)->getFirstChild();
332                 obj; obj = obj->getNextObject())
333             type |= getHitObjects(obj, x, y);
334
335     int cx, cy, cw, ch;
336     object->getAbsolutePosition(&cx, &cy);
337     object->getSize(&cw, &ch);
338     if (x >= cx && x < cx + cw && y >= cy && y < cy + ch)
339         type |= object->getType();
340     return type;
341 }
342
343 void fgPopup::applySize(puObject *object)
344 {
345     // compound plib widgets use setUserData() for internal purposes, so refuse
346     // to descend into anything that has other bits set than the following
347     const int validUserData = PUCLASS_VALUE|PUCLASS_OBJECT|PUCLASS_GROUP|PUCLASS_INTERFACE
348             |PUCLASS_FRAME|PUCLASS_TEXT|PUCLASS_BUTTON|PUCLASS_ONESHOT|PUCLASS_INPUT
349             |PUCLASS_ARROW|PUCLASS_DIAL|PUCLASS_POPUP;
350
351     int type = object->getType();
352     if (type & PUCLASS_GROUP && !(type & ~validUserData))
353         for (puObject *obj = ((puGroup *)object)->getFirstChild();
354                 obj; obj = obj->getNextObject())
355             applySize(obj);
356
357     GUIInfo *info = (GUIInfo *)object->getUserData();
358     if (!info)
359         return;
360
361     SGPropertyNode *n = info->node;
362     if (!n) {
363         SG_LOG(SG_GENERAL, SG_ALERT, "fgPopup::applySize: no props");
364         return;
365     }
366     int x = n->getIntValue("x");
367     int y = n->getIntValue("y");
368     int w = n->getIntValue("width", 4);
369     int h = n->getIntValue("height", 4);
370     object->setPosition(x, y);
371     object->setSize(w, h);
372 }
373
374 ////////////////////////////////////////////////////////////////////////
375
376 void FGPUIDialog::ConditionalObject::update(FGPUIDialog* aDlg)
377 {
378   if (_name == "visible") {
379     bool wasVis = _pu->isVisible() != 0;
380     bool newVis = test();
381     
382     if (newVis == wasVis) {
383       return;
384     }
385     
386     if (newVis) { // puObject needs a setVisible. Oh well.
387     _pu->reveal();
388     } else {
389       _pu->hide();
390     }
391   } else if (_name == "enable") {
392     bool wasEnabled = _pu->isActive() != 0;
393     bool newEnable = test();
394     
395     if (wasEnabled == newEnable) {
396       return;
397     }
398     
399     if (newEnable) {
400       _pu->activate();
401     } else {
402       _pu->greyOut();
403     }
404   }
405   
406   aDlg->setNeedsLayout();
407 }
408 \f////////////////////////////////////////////////////////////////////////
409 // Callbacks.
410 ////////////////////////////////////////////////////////////////////////
411
412 /**
413  * Action callback.
414  */
415 static void
416 action_callback (puObject *object)
417 {
418     GUIInfo *info = (GUIInfo *)object->getUserData();
419     NewGUI *gui = (NewGUI *)globals->get_subsystem("gui");
420     gui->setActiveDialog(info->dialog);
421     int nBindings = info->bindings.size();
422     for (int i = 0; i < nBindings; i++) {
423         info->bindings[i]->fire();
424         if (gui->getActiveDialog() == 0)
425             break;
426     }
427     gui->setActiveDialog(0);
428 }
429
430
431 \f
432 ////////////////////////////////////////////////////////////////////////
433 // Static helper functions.
434 ////////////////////////////////////////////////////////////////////////
435
436 /**
437  * Copy a property value to a PUI object.
438  */
439 static void
440 copy_to_pui (SGPropertyNode *node, puObject *object)
441 {
442     using namespace simgear;
443     GUIInfo *info = (GUIInfo *)object->getUserData();
444     if (!info) {
445         SG_LOG(SG_GENERAL, SG_ALERT, "dialog: widget without GUIInfo!");
446         return;   // this can't really happen
447     }
448
449     // Treat puText objects specially, so their "values" can be set
450     // from properties.
451     if (object->getType() & PUCLASS_TEXT) {
452         if (info->fmt_type != f_INVALID)
453             info->apply_format(node);
454         else
455             info->text = node->getStringValue();
456
457         object->setLabel(info->text.c_str());
458         return;
459     }
460
461     switch (node->getType()) {
462     case props::BOOL:
463     case props::INT:
464     case props::LONG:
465         object->setValue(node->getIntValue());
466         break;
467     case props::FLOAT:
468     case props::DOUBLE:
469         object->setValue(node->getFloatValue());
470         break;
471     default:
472         info->text = node->getStringValue();
473         object->setValue(info->text.c_str());
474         break;
475     }
476 }
477
478
479 static void
480 copy_from_pui (puObject *object, SGPropertyNode *node)
481 {
482     using namespace simgear;
483     // puText objects are immutable, so should not be copied out
484     if (object->getType() & PUCLASS_TEXT)
485         return;
486
487     switch (node->getType()) {
488     case props::BOOL:
489     case props::INT:
490     case props::LONG:
491         node->setIntValue(object->getIntegerValue());
492         break;
493     case props::FLOAT:
494         node->setFloatValue(object->getFloatValue());
495         break;
496     case props::DOUBLE:
497     {
498         // puObject only provides float, not double, which causes precision/rounding issues
499         // with some numerical values (try "114.2").
500         // Work around: obtain string value, and manually convert with proper double precision.
501         const char *s = object->getStringValue();
502         node->setDoubleValue(atof(s));
503         break;
504     }
505     default:
506         const char *s = object->getStringValue();
507         if (s)
508             node->setStringValue(s);
509         break;
510     }
511 }
512
513
514 \f
515 ////////////////////////////////////////////////////////////////////////
516 // Implementation of FGDialog.
517 ////////////////////////////////////////////////////////////////////////
518
519 FGPUIDialog::FGPUIDialog (SGPropertyNode *props) :
520     FGDialog(props),
521     _object(0),
522     _gui((NewGUI *)globals->get_subsystem("gui")),
523     _props(props),
524     _needsRelayout(false)
525 {
526     _module = string("__dlg:") + props->getStringValue("name", "[unnamed]");
527         
528     SGPropertyNode *nasal = props->getNode("nasal");
529     if (nasal) {
530         _nasal_close = nasal->getNode("close");
531         SGPropertyNode *open = nasal->getNode("open");
532         if (open) {
533             const char *s = open->getStringValue();
534             FGNasalSys *nas = (FGNasalSys *)globals->get_subsystem("nasal");
535             if (nas)
536                 nas->createModule(_module.c_str(), _module.c_str(), s, strlen(s), props);
537         }
538     }
539     display(props);
540 }
541
542 FGPUIDialog::~FGPUIDialog ()
543 {
544     int x, y;
545     _object->getAbsolutePosition(&x, &y);
546     _props->setIntValue("lastx", x);
547     _props->setIntValue("lasty", y);
548
549     FGNasalSys *nas = (FGNasalSys *)globals->get_subsystem("nasal");
550     if (nas) {
551         if (_nasal_close) {
552             const char *s = _nasal_close->getStringValue();
553             nas->createModule(_module.c_str(), _module.c_str(), s, strlen(s), _props);
554         }
555         nas->deleteModule(_module.c_str());
556     }
557
558     puDeleteObject(_object);
559
560     unsigned int i;
561                                 // Delete all the info objects we
562                                 // were forced to keep around because
563                                 // PUI cannot delete its own user data.
564     for (i = 0; i < _info.size(); i++) {
565         delete (GUIInfo *)_info[i];
566         _info[i] = 0;
567     }
568                                 // Finally, delete the property links.
569     for (i = 0; i < _propertyObjects.size(); i++) {
570         delete _propertyObjects[i];
571         _propertyObjects[i] = 0;
572     }
573 }
574
575 void
576 FGPUIDialog::updateValues (const char *objectName)
577 {
578     if (objectName && !objectName[0])
579         objectName = 0;
580
581   for (unsigned int i = 0; i < _propertyObjects.size(); i++) {
582     const string &name = _propertyObjects[i]->name;
583     if (objectName && name != objectName) {
584       continue;
585     }
586     
587     puObject *widget = _propertyObjects[i]->object;
588     int widgetType = widget->getType();
589     if (widgetType & PUCLASS_LIST) {
590       GUI_ID* gui_id = dynamic_cast<GUI_ID *>(widget);
591       if (gui_id && (gui_id->id & FGCLASS_LIST)) {
592         fgList *pl = static_cast<fgList*>(widget);
593         pl->update();
594       } else {
595         copy_to_pui(_propertyObjects[i]->node, widget);
596       }
597     } else if (widgetType & PUCLASS_COMBOBOX) {
598       fgComboBox* combo = static_cast<fgComboBox*>(widget);
599       combo->update();
600     } else {
601       copy_to_pui(_propertyObjects[i]->node, widget);
602     }
603   } // of property objects iteration
604 }
605
606 void
607 FGPUIDialog::applyValues (const char *objectName)
608 {
609     if (objectName && !objectName[0])
610         objectName = 0;
611
612     for (unsigned int i = 0; i < _propertyObjects.size(); i++) {
613         const string &name = _propertyObjects[i]->name;
614         if (objectName && name != objectName)
615             continue;
616
617         copy_from_pui(_propertyObjects[i]->object,
618                       _propertyObjects[i]->node);
619     }
620 }
621
622 void
623 FGPUIDialog::update ()
624 {
625     for (unsigned int i = 0; i < _liveObjects.size(); i++) {
626         puObject *obj = _liveObjects[i]->object;
627         if (obj->getType() & PUCLASS_INPUT && ((puInput *)obj)->isAcceptingInput())
628             continue;
629
630         copy_to_pui(_liveObjects[i]->node, obj);
631     }
632     
633   for (unsigned int j=0; j < _conditionalObjects.size(); ++j) {
634     _conditionalObjects[j]->update(this);
635   }
636   
637   if (_needsRelayout) {
638     relayout();
639   }
640 }
641
642 void
643 FGPUIDialog::display (SGPropertyNode *props)
644 {
645     if (_object != 0) {
646         SG_LOG(SG_GENERAL, SG_ALERT, "This widget is already active");
647         return;
648     }
649
650     int screenw = globals->get_props()->getIntValue("/sim/startup/xsize");
651     int screenh = globals->get_props()->getIntValue("/sim/startup/ysize");
652
653     bool userx = props->hasValue("x");
654     bool usery = props->hasValue("y");
655     bool userw = props->hasValue("width");
656     bool userh = props->hasValue("height");
657
658     // Let the layout widget work in the same property subtree.
659     LayoutWidget wid(props);
660
661     SGPropertyNode *fontnode = props->getNode("font");
662     if (fontnode) {
663         FGFontCache *fc = globals->get_fontcache();
664         _font = fc->get(fontnode);
665     } else {
666         _font = _gui->getDefaultFont();
667     }
668     wid.setDefaultFont(_font, int(_font->getPointSize()));
669
670     int pw = 0, ph = 0;
671     int px, py, savex, savey;
672     if (!userw || !userh)
673         wid.calcPrefSize(&pw, &ph);
674     pw = props->getIntValue("width", pw);
675     ph = props->getIntValue("height", ph);
676     px = savex = props->getIntValue("x", (screenw - pw) / 2);
677     py = savey = props->getIntValue("y", (screenh - ph) / 2);
678
679     // Negative x/y coordinates are interpreted as distance from the top/right
680     // corner rather than bottom/left.
681     if (userx && px < 0)
682         px = screenw - pw + px;
683     if (usery && py < 0)
684         py = screenh - ph + py;
685
686     // Define "x", "y", "width" and/or "height" in the property tree if they
687     // are not specified in the configuration file.
688     wid.layout(px, py, pw, ph);
689
690     // Use the dimension and location properties as specified in the
691     // configuration file or from the layout widget.
692     _object = makeObject(props, screenw, screenh);
693
694     // Remove automatically generated properties, so the layout looks
695     // the same next time around, or restore x and y to preserve negative coords.
696     if (userx)
697         props->setIntValue("x", savex);
698     else
699         props->removeChild("x");
700
701     if (usery)
702         props->setIntValue("y", savey);
703     else
704         props->removeChild("y");
705
706     if (!userw) props->removeChild("width");
707     if (!userh) props->removeChild("height");
708
709     if (_object != 0) {
710         _object->reveal();
711     } else {
712         SG_LOG(SG_GENERAL, SG_ALERT, "Widget "
713                << props->getStringValue("name", "[unnamed]")
714                << " does not contain a proper GUI definition");
715     }
716 }
717
718 puObject *
719 FGPUIDialog::makeObject (SGPropertyNode *props, int parentWidth, int parentHeight)
720 {
721     if (!props->getBoolValue("enabled", true))
722         return 0;
723
724     bool presetSize = props->hasValue("width") && props->hasValue("height");
725     int width = props->getIntValue("width", parentWidth);
726     int height = props->getIntValue("height", parentHeight);
727     int x = props->getIntValue("x", (parentWidth - width) / 2);
728     int y = props->getIntValue("y", (parentHeight - height) / 2);
729     string type = props->getName();
730
731     if (type.empty())
732         type = "dialog";
733
734     if (type == "dialog") {
735         puPopup *obj;
736         bool draggable = props->getBoolValue("draggable", true);
737         bool resizable = props->getBoolValue("resizable", false);
738         if (props->getBoolValue("modal", false))
739             obj = new puDialogBox(x, y);
740         else
741             obj = new fgPopup(x, y, resizable, draggable);
742         setupGroup(obj, props, width, height, true);
743         setColor(obj, props);
744         return obj;
745
746     } else if (type == "group") {
747         puGroup *obj = new puGroup(x, y);
748         setupGroup(obj, props, width, height, false);
749         setColor(obj, props);
750         return obj;
751
752     } else if (type == "frame") {
753         puGroup *obj = new puGroup(x, y);
754         setupGroup(obj, props, width, height, true);
755         setColor(obj, props);
756         return obj;
757
758     } else if (type == "hrule" || type == "vrule") {
759         puFrame *obj = new puFrame(x, y, x + width, y + height);
760         obj->setBorderThickness(0);
761         setupObject(obj, props);
762         setColor(obj, props, BACKGROUND|FOREGROUND|HIGHLIGHT);
763         return obj;
764
765     } else if (type == "list") {
766         int slider_width = props->getIntValue("slider", 20);
767         fgList *obj = new fgList(x, y, x + width, y + height, props, slider_width);
768         if (presetSize)
769             obj->setSize(width, height);
770         setupObject(obj, props);
771         setColor(obj, props);
772         return obj;
773
774     } else if (type == "airport-list") {
775         AirportList *obj = new AirportList(x, y, x + width, y + height);
776         if (presetSize)
777             obj->setSize(width, height);
778         setupObject(obj, props);
779         setColor(obj, props);
780         return obj;
781
782     } else if (type == "property-list") {
783         PropertyList *obj = new PropertyList(x, y, x + width, y + height, globals->get_props());
784         if (presetSize)
785             obj->setSize(width, height);
786         setupObject(obj, props);
787         setColor(obj, props);
788         return obj;
789
790     } else if (type == "input") {
791         puInput *obj = new puInput(x, y, x + width, y + height);
792         setupObject(obj, props);
793         setColor(obj, props, FOREGROUND|LABEL);
794         return obj;
795
796     } else if (type == "text") {
797         puText *obj = new puText(x, y);
798         setupObject(obj, props);
799
800         // Layed-out objects need their size set, and non-layout ones
801         // get a different placement.
802         if (presetSize)
803             obj->setSize(width, height);
804         else
805             obj->setLabelPlace(PUPLACE_LABEL_DEFAULT);
806         setColor(obj, props, LABEL);
807         return obj;
808
809     } else if (type == "checkbox") {
810         puButton *obj;
811         obj = new puButton(x, y, x + width, y + height, PUBUTTON_XCHECK);
812         setupObject(obj, props);
813         setColor(obj, props, FOREGROUND|LABEL);
814         return obj;
815
816     } else if (type == "radio") {
817         puButton *obj;
818         obj = new puButton(x, y, x + width, y + height, PUBUTTON_CIRCLE);
819         setupObject(obj, props);
820         setColor(obj, props, FOREGROUND|LABEL);
821         return obj;
822
823     } else if (type == "button") {
824         puButton *obj;
825         const char *legend = props->getStringValue("legend", "[none]");
826         if (props->getBoolValue("one-shot", true))
827             obj = new puOneShot(x, y, legend);
828         else
829             obj = new puButton(x, y, legend);
830         if (presetSize)
831             obj->setSize(width, height);
832         setupObject(obj, props);
833         setColor(obj, props);
834         return obj;
835     } else if (type == "map") {
836         MapWidget* mapWidget = new MapWidget(x, y, x + width, y + height);
837         setupObject(mapWidget, props);
838         return mapWidget;
839     } else if (type == "canvas") {
840         CanvasWidget* canvasWidget = new CanvasWidget( x, y,
841                                                        x + width, y + height,
842                                                        props,
843                                                        _module );
844         setupObject(canvasWidget, props);
845         return canvasWidget;
846     } else if (type == "combo") {
847         fgComboBox *obj = new fgComboBox(x, y, x + width, y + height, props,
848                 props->getBoolValue("editable", false));
849         setupObject(obj, props);
850         setColor(obj, props, EDITFIELD);
851         return obj;
852
853     } else if (type == "slider") {
854         bool vertical = props->getBoolValue("vertical", false);
855         puSlider *obj = new puSlider(x, y, (vertical ? height : width), vertical);
856         obj->setMinValue(props->getFloatValue("min", 0.0));
857         obj->setMaxValue(props->getFloatValue("max", 1.0));
858         obj->setStepSize(props->getFloatValue("step"));
859         obj->setSliderFraction(props->getFloatValue("fraction"));
860 #if PLIB_VERSION > 185
861         obj->setPageStepSize(props->getFloatValue("pagestep"));
862 #endif
863         setupObject(obj, props);
864         if (presetSize)
865             obj->setSize(width, height);
866         setColor(obj, props, FOREGROUND|LABEL);
867         return obj;
868
869     } else if (type == "dial") {
870         puDial *obj = new puDial(x, y, width);
871         obj->setMinValue(props->getFloatValue("min", 0.0));
872         obj->setMaxValue(props->getFloatValue("max", 1.0));
873         obj->setWrap(props->getBoolValue("wrap", true));
874         setupObject(obj, props);
875         setColor(obj, props, FOREGROUND|LABEL);
876         return obj;
877
878     } else if (type == "textbox") {
879         int slider_width = props->getIntValue("slider", 20);
880         int wrap = props->getBoolValue("wrap", true);
881 #if PLIB_VERSION > 185
882         puaLargeInput *obj = new puaLargeInput(x, y,
883                 x + width, x + height, 11, slider_width, wrap);
884 #else
885         puaLargeInput *obj = new puaLargeInput(x, y,
886                 x + width, x + height, 2, slider_width, wrap);
887 #endif
888
889         if (props->getBoolValue("editable"))
890             obj->enableInput();
891         else
892             obj->disableInput();
893
894         if (presetSize)
895             obj->setSize(width, height);
896         setupObject(obj, props);
897         setColor(obj, props, FOREGROUND|LABEL);
898
899         int top = props->getIntValue("top-line", 0);
900         obj->setTopLineInWindow(top < 0 ? unsigned(-1) >> 1 : top);
901         return obj;
902
903     } else if (type == "select") {
904         fgSelectBox *obj = new fgSelectBox(x, y, x + width, y + height, props);
905         setupObject(obj, props);
906         setColor(obj, props, EDITFIELD);
907         return obj;
908     } else if (type == "waypointlist") {
909         ScrolledWaypointList* obj = new ScrolledWaypointList(x, y, width, height);
910         setupObject(obj, props);
911         return obj;
912     } else {
913         return 0;
914     }
915 }
916
917 void
918 FGPUIDialog::setupObject (puObject *object, SGPropertyNode *props)
919 {
920     GUIInfo *info = new GUIInfo(this);
921     object->setUserData(info);
922     _info.push_back(info);
923     object->setLabelPlace(PUPLACE_CENTERED_RIGHT);
924     object->makeReturnDefault(props->getBoolValue("default"));
925     info->node = props;
926
927     if (props->hasValue("legend")) {
928         info->legend = props->getStringValue("legend");
929         object->setLegend(info->legend.c_str());
930     }
931
932     if (props->hasValue("label")) {
933         info->label = props->getStringValue("label");
934         object->setLabel(info->label.c_str());
935     }
936
937     if (props->hasValue("border"))
938         object->setBorderThickness( props->getIntValue("border", 2) );
939
940     if (SGPropertyNode *nft = props->getNode("font", false)) {
941        FGFontCache *fc = globals->get_fontcache();
942        puFont *lfnt = fc->get(nft);
943        object->setLabelFont(*lfnt);
944        object->setLegendFont(*lfnt);
945     } else {
946        object->setLabelFont(*_font);
947     }
948
949     if (props->hasChild("visible")) {
950       ConditionalObject* cnd = new ConditionalObject("visible", object);
951       cnd->setCondition(sgReadCondition(globals->get_props(), props->getChild("visible")));
952       _conditionalObjects.push_back(cnd);
953     }
954
955     if (props->hasChild("enable")) {
956       ConditionalObject* cnd = new ConditionalObject("enable", object);
957       cnd->setCondition(sgReadCondition(globals->get_props(), props->getChild("enable")));
958       _conditionalObjects.push_back(cnd);
959     }
960
961     string type = props->getName();
962     if (type == "input" && props->getBoolValue("live"))
963         object->setDownCallback(action_callback);
964
965     if (type == "text") {
966         const char *format = props->getStringValue("format", 0);
967         if (format) {
968             info->fmt_type = validate_format(format);
969             if (info->fmt_type != f_INVALID)
970                 info->format = format;
971             else
972                 SG_LOG(SG_GENERAL, SG_ALERT, "DIALOG: invalid <format> '"
973                         << format << '\'');
974         }
975     }
976
977     if (props->hasValue("property")) {
978         const char *name = props->getStringValue("name");
979         if (name == 0)
980             name = "";
981         const char *propname = props->getStringValue("property");
982         SGPropertyNode_ptr node = fgGetNode(propname, true);
983         if (type == "map") {
984           // mapWidget binds to a sub-tree of properties, and
985           // ignores the puValue mechanism, so special case things here
986           MapWidget* mw = static_cast<MapWidget*>(object);
987           mw->setProperty(node);
988         } else {
989             // normal widget, creating PropertyObject
990             copy_to_pui(node, object);
991             PropertyObject *po = new PropertyObject(name, object, node);
992             _propertyObjects.push_back(po);
993             if (props->getBoolValue("live"))
994                 _liveObjects.push_back(po);
995         }
996         
997
998     }
999
1000     SGPropertyNode *dest = fgGetNode("/sim/bindings/gui", true);
1001     vector<SGPropertyNode_ptr> bindings = props->getChildren("binding");
1002     if (bindings.size() > 0) {
1003         info->key = props->getIntValue("keynum", -1);
1004         if (props->hasValue("key"))
1005             info->key = getKeyCode(props->getStringValue("key", ""));
1006
1007         for (unsigned int i = 0; i < bindings.size(); i++) {
1008             unsigned int j = 0;
1009             SGPropertyNode_ptr binding;
1010             while (dest->getChild("binding", j))
1011                 j++;
1012
1013             const char *cmd = bindings[i]->getStringValue("command");
1014             if (!strcmp(cmd, "nasal"))
1015                 bindings[i]->setStringValue("module", _module.c_str());
1016
1017             binding = dest->getChild("binding", j, true);
1018             copyProperties(bindings[i], binding);
1019             info->bindings.push_back(new SGBinding(binding, globals->get_props()));
1020         }
1021         object->setCallback(action_callback);
1022     }
1023 }
1024
1025 void
1026 FGPUIDialog::setupGroup(puGroup *group, SGPropertyNode *props,
1027        int width, int height, bool makeFrame)
1028 {
1029     setupObject(group, props);
1030
1031     if (makeFrame) {
1032         puFrame* f = new puFrame(0, 0, width, height);
1033         setColor(f, props);
1034     }
1035
1036     int nChildren = props->nChildren();
1037     for (int i = 0; i < nChildren; i++)
1038         makeObject(props->getChild(i), width, height);
1039     group->close();
1040 }
1041
1042 void
1043 FGPUIDialog::setColor(puObject *object, SGPropertyNode *props, int which)
1044 {
1045     string type = props->getName();
1046     if (type.empty())
1047         type = "dialog";
1048     if (type == "textbox" && props->getBoolValue("editable"))
1049         type += "-editable";
1050
1051     FGColor c(_gui->getColor("background"));
1052     c.merge(_gui->getColor(type));
1053     c.merge(props->getNode("color"));
1054     if (c.isValid())
1055         object->setColourScheme(c.red(), c.green(), c.blue(), c.alpha());
1056
1057     const struct {
1058         int mask;
1059         int id;
1060         const char *name;
1061         const char *cname;
1062     } pucol[] = {
1063         { BACKGROUND, PUCOL_BACKGROUND, "background", "color-background" },
1064         { FOREGROUND, PUCOL_FOREGROUND, "foreground", "color-foreground" },
1065         { HIGHLIGHT,  PUCOL_HIGHLIGHT,  "highlight",  "color-highlight" },
1066         { LABEL,      PUCOL_LABEL,      "label",      "color-label" },
1067         { LEGEND,     PUCOL_LEGEND,     "legend",     "color-legend" },
1068         { MISC,       PUCOL_MISC,       "misc",       "color-misc" },
1069         { EDITFIELD,  PUCOL_EDITFIELD,  "editfield",  "color-editfield" },
1070     };
1071
1072     const int numcol = sizeof(pucol) / sizeof(pucol[0]);
1073
1074     for (int i = 0; i < numcol; i++) {
1075         bool dirty = false;
1076         c.clear();
1077         c.setAlpha(1.0);
1078
1079         dirty |= c.merge(_gui->getColor(type + '-' + pucol[i].name));
1080         if (which & pucol[i].mask)
1081             dirty |= c.merge(props->getNode("color"));
1082
1083         if ((pucol[i].mask == LABEL) && !c.isValid())
1084             dirty |= c.merge(_gui->getColor("label"));
1085
1086         dirty |= c.merge(props->getNode(pucol[i].cname));
1087
1088         if (c.isValid() && dirty)
1089             object->setColor(pucol[i].id, c.red(), c.green(), c.blue(), c.alpha());
1090     }
1091 }
1092
1093
1094 static struct {
1095     const char *name;
1096     int key;
1097 } keymap[] = {
1098     {"backspace", 8},
1099     {"tab", 9},
1100     {"return", 13},
1101     {"enter", 13},
1102     {"esc", 27},
1103     {"escape", 27},
1104     {"space", ' '},
1105     {"&amp;", '&'},
1106     {"and", '&'},
1107     {"&lt;", '<'},
1108     {"&gt;", '>'},
1109     {"f1", PU_KEY_F1},
1110     {"f2", PU_KEY_F2},
1111     {"f3", PU_KEY_F3},
1112     {"f4", PU_KEY_F4},
1113     {"f5", PU_KEY_F5},
1114     {"f6", PU_KEY_F6},
1115     {"f7", PU_KEY_F7},
1116     {"f8", PU_KEY_F8},
1117     {"f9", PU_KEY_F9},
1118     {"f10", PU_KEY_F10},
1119     {"f11", PU_KEY_F11},
1120     {"f12", PU_KEY_F12},
1121     {"left", PU_KEY_LEFT},
1122     {"up", PU_KEY_UP},
1123     {"right", PU_KEY_RIGHT},
1124     {"down", PU_KEY_DOWN},
1125     {"pageup", PU_KEY_PAGE_UP},
1126     {"pagedn", PU_KEY_PAGE_DOWN},
1127     {"home", PU_KEY_HOME},
1128     {"end", PU_KEY_END},
1129     {"insert", PU_KEY_INSERT},
1130     {0, -1},
1131 };
1132
1133 int
1134 FGPUIDialog::getKeyCode(const char *str)
1135 {
1136     enum {
1137         CTRL = 0x1,
1138         SHIFT = 0x2,
1139         ALT = 0x4,
1140     };
1141
1142     while (*str == ' ')
1143         str++;
1144
1145     char *buf = new char[strlen(str) + 1];
1146     strcpy(buf, str);
1147     char *s = buf + strlen(buf);
1148     while (s > str && s[-1] == ' ')
1149         s--;
1150     *s = 0;
1151     s = buf;
1152
1153     int mod = 0;
1154     while (1) {
1155         if (!strncmp(s, "Ctrl-", 5) || !strncmp(s, "CTRL-", 5))
1156             s += 5, mod |= CTRL;
1157         else if (!strncmp(s, "Shift-", 6) || !strncmp(s, "SHIFT-", 6))
1158             s += 6, mod |= SHIFT;
1159         else if (!strncmp(s, "Alt-", 4) || !strncmp(s, "ALT-", 4))
1160             s += 4, mod |= ALT;
1161         else
1162             break;
1163     }
1164
1165     int key = -1;
1166     if (strlen(s) == 1 && isascii(*s)) {
1167         key = *s;
1168         if (mod & SHIFT)
1169             key = toupper(key);
1170         if (mod & CTRL)
1171             key = toupper(key) - '@';
1172         /* if (mod & ALT)
1173             ;   // Alt not propagated to the gui
1174         */
1175     } else {
1176         for (char *t = s; *t; t++)
1177             *t = tolower(*t);
1178         for (int i = 0; keymap[i].name; i++) {
1179             if (!strcmp(s, keymap[i].name)) {
1180                 key = keymap[i].key;
1181                 break;
1182             }
1183         }
1184     }
1185     delete[] buf;
1186     return key;
1187 }
1188
1189 void\fFGPUIDialog::relayout()
1190 {
1191   _needsRelayout = false;
1192   
1193   int screenw = globals->get_props()->getIntValue("/sim/startup/xsize");
1194   int screenh = globals->get_props()->getIntValue("/sim/startup/ysize");
1195
1196   bool userx = _props->hasValue("x");
1197   bool usery = _props->hasValue("y");
1198   bool userw = _props->hasValue("width");
1199   bool userh = _props->hasValue("height");
1200
1201   // Let the layout widget work in the same property subtree.
1202   LayoutWidget wid(_props);
1203   wid.setDefaultFont(_font, int(_font->getPointSize()));
1204
1205   int pw = 0, ph = 0;
1206   int px, py, savex, savey;
1207   if (!userw || !userh) {
1208     wid.calcPrefSize(&pw, &ph);
1209   }
1210   
1211   pw = _props->getIntValue("width", pw);
1212   ph = _props->getIntValue("height", ph);
1213   px = savex = _props->getIntValue("x", (screenw - pw) / 2);
1214   py = savey = _props->getIntValue("y", (screenh - ph) / 2);
1215
1216   // Negative x/y coordinates are interpreted as distance from the top/right
1217   // corner rather than bottom/left.
1218   if (userx && px < 0)
1219     px = screenw - pw + px;
1220   if (usery && py < 0)
1221     py = screenh - ph + py;
1222
1223   // Define "x", "y", "width" and/or "height" in the property tree if they
1224   // are not specified in the configuration file.
1225   wid.layout(px, py, pw, ph);
1226
1227   applySize(_object);
1228   
1229   // Remove automatically generated properties, so the layout looks
1230   // the same next time around, or restore x and y to preserve negative coords.
1231   if (userx)
1232       _props->setIntValue("x", savex);
1233   else
1234       _props->removeChild("x");
1235
1236   if (usery)
1237       _props->setIntValue("y", savey);
1238   else
1239       _props->removeChild("y");
1240
1241   if (!userw) _props->removeChild("width");
1242   if (!userh) _props->removeChild("height");
1243 }
1244
1245 void
1246 FGPUIDialog::applySize(puObject *object)
1247 {
1248   // compound plib widgets use setUserData() for internal purposes, so refuse
1249   // to descend into anything that has other bits set than the following
1250   const int validUserData = PUCLASS_VALUE|PUCLASS_OBJECT|PUCLASS_GROUP|PUCLASS_INTERFACE
1251           |PUCLASS_FRAME|PUCLASS_TEXT|PUCLASS_BUTTON|PUCLASS_ONESHOT|PUCLASS_INPUT
1252           |PUCLASS_ARROW|PUCLASS_DIAL|PUCLASS_POPUP;
1253
1254   int type = object->getType();
1255   if ((type & PUCLASS_GROUP) && !(type & ~validUserData)) {
1256     puObject* c = ((puGroup *)object)->getFirstChild();
1257     for (; c != NULL; c = c->getNextObject()) {
1258       applySize(c);
1259     } // of child iteration
1260   } // of group object case
1261   
1262   GUIInfo *info = (GUIInfo *)object->getUserData();
1263   if (!info)
1264     return;
1265
1266   SGPropertyNode *n = info->node;
1267   if (!n) {
1268     SG_LOG(SG_GENERAL, SG_ALERT, "FGDialog::applySize: no props");
1269     return;
1270   }
1271   
1272   int x = n->getIntValue("x");
1273   int y = n->getIntValue("y");
1274   int w = n->getIntValue("width", 4);
1275   int h = n->getIntValue("height", 4);
1276   object->setPosition(x, y);
1277   object->setSize(w, h);
1278 }
1279
1280 ////////////////////////////////////////////////////////////////////////
1281 // Implementation of FGDialog::PropertyObject.
1282 ////////////////////////////////////////////////////////////////////////
1283
1284 FGPUIDialog::PropertyObject::PropertyObject(const char *n,
1285         puObject *o, SGPropertyNode_ptr p) :
1286     name(n),
1287     object(o),
1288     node(p)
1289 {
1290 }
1291
1292
1293
1294 \f
1295 ////////////////////////////////////////////////////////////////////////
1296 // Implementation of fgValueList and derived pui widgets
1297 ////////////////////////////////////////////////////////////////////////
1298
1299
1300 fgValueList::fgValueList(SGPropertyNode *p) :
1301     _props(p)
1302 {
1303     make_list();
1304 }
1305
1306 void
1307 fgValueList::update()
1308 {
1309     destroy_list();
1310     make_list();
1311 }
1312
1313 fgValueList::~fgValueList()
1314 {
1315     destroy_list();
1316 }
1317
1318 void
1319 fgValueList::make_list()
1320 {
1321   SGPropertyNode_ptr values = _props;
1322   const char* vname = "value";
1323   
1324   if (_props->hasChild("properties")) {
1325     // dynamic values, read from a property's children
1326     const char* path = _props->getStringValue("properties");
1327     values = fgGetNode(path, true);
1328   }
1329   
1330   if (_props->hasChild("property-name")) {
1331     vname = _props->getStringValue("property-name");
1332   }
1333   
1334   vector<SGPropertyNode_ptr> value_nodes = values->getChildren(vname);
1335   _list = new char *[value_nodes.size() + 1];
1336   unsigned int i;
1337   for (i = 0; i < value_nodes.size(); i++) {
1338     _list[i] = strdup((char *)value_nodes[i]->getStringValue());
1339   }
1340   _list[i] = 0;
1341 }
1342
1343 void
1344 fgValueList::destroy_list()
1345 {
1346     for (int i = 0; _list[i] != 0; i++)
1347         if (_list[i])
1348             free(_list[i]);
1349     delete[] _list;
1350 }
1351
1352
1353
1354 void
1355 fgList::update()
1356 {
1357     fgValueList::update();
1358     int top = getTopItem();
1359     newList(_list);
1360     setTopItem(top);
1361 }
1362
1363 void fgComboBox::update()
1364 {
1365   if (_inHit) {
1366     return;
1367   }
1368   
1369   std::string curValue(getStringValue());
1370   fgValueList::update();
1371   newList(_list);
1372   int currentItem = puaComboBox::getCurrentItem();
1373
1374   
1375 // look for the previous value, in the new list
1376   for (int i = 0; _list[i] != 0; i++) {
1377     if (_list[i] == curValue) {
1378     // don't set the current item again; this is important to avoid
1379     // recursion here if the combo callback ultimately causes another dialog-update
1380       if (i != currentItem) {
1381         puaComboBox::setCurrentItem(i);
1382       }
1383       
1384       return;
1385     }
1386   } // of list values iteration
1387   
1388 // cound't find current item, default to first
1389   if (currentItem != 0) {
1390     puaComboBox::setCurrentItem(0);
1391   }
1392 }
1393
1394 int fgComboBox::checkHit(int b, int up, int x, int y)
1395 {
1396   _inHit = true;
1397   int r = puaComboBox::checkHit(b, up, x, y);
1398   _inHit = false;
1399   return r;
1400 }
1401
1402 void fgComboBox::setSize(int w, int h)
1403 {
1404   puaComboBox::setSize(w, h);
1405   recalc_bbox();
1406 }
1407
1408 void fgComboBox::recalc_bbox()
1409 {
1410 // bug-fix for issue #192
1411 // http://code.google.com/p/flightgear-bugs/issues/detail?id=192
1412 // puaComboBox is including the height of its popupMenu in the height
1413 // computation, which breaks layout computations.
1414 // this implementation skips popup-menus
1415   
1416   puBox contents;
1417   contents.empty();
1418   
1419   for (puObject *bo = dlist; bo != NULL; bo = bo -> getNextObject()) {
1420     if (bo->getType() & PUCLASS_POPUPMENU) {
1421       continue;
1422     }
1423     
1424     contents.extend (bo -> getBBox()) ;
1425   }
1426   
1427   abox.max[0] = abox.min[0] + contents.max[0] ;
1428   abox.max[1] = abox.min[1] + contents.max[1] ;
1429
1430   puObject::recalc_bbox () ;
1431
1432 }
1433
1434 // end of FGPUIDialog.cxx