remove README.Protocol and add a README that refers to the "real"
[fg:toms-fgdata.git] / Nasal / gui.nas
1 ##
2 # Pop up a "tip" dialog for a moment, then remove it.  The delay in
3 # seconds can be specified as the second argument.  The default is 1
4 # second.  Note that the tip dialog is a shared resource.  If
5 # someone else comes along and wants to pop a tip up before your delay
6 # is finished, you lose. :)
7 #
8 popupTip = func {
9     delay = if(size(arg) > 1) {arg[1]} else {DELAY};
10     tmpl = { name : "PopTip", modal : 0, layout : "hbox",
11              y: screenHProp.getValue() - 140,
12              text : { label : arg[0], padding : 6 } };
13
14     popdown();
15     fgcommand("dialog-new", props.Node.new(tmpl));
16     fgcommand("dialog-show", tipArg);
17
18     currTimer = currTimer + 1;
19     thisTimer = currTimer;
20
21     # Final argument is a flag to use "real" time, not simulated time
22     settimer(func { if(currTimer == thisTimer) { popdown() } }, DELAY, 1);
23 }
24
25 showDialog = func {
26     fgcommand("dialog-show",
27               props.Node.new({ "dialog-name" : arg[0]}));
28 }
29
30 ##
31 # Enable/disable named menu entry
32 #
33 menuEnable = func(searchname, state) {
34     foreach (menu; props.globals.getNode("/sim/menubar/default").getChildren("menu")) {
35         foreach (name; menu.getChildren("name")) {
36             if (name.getValue() == searchname) {
37                 menu.getNode("enabled").setBoolValue(state);
38             }
39         }
40         foreach (item; menu.getChildren("item")) {
41             foreach (name; item.getChildren("name")) {
42                 if (name.getValue() == searchname) {
43                     item.getNode("enabled").setBoolValue(state);
44                 }
45             }
46         }
47     }
48 }
49
50
51
52 ########################################################################
53 # Private Stuff:
54 ########################################################################
55
56 ##
57 # Initialize property nodes via a timer, to insure the props module is
58 # loaded.  See notes in view.nas.  Simply cache the screen height
59 # property and the argument for the "dialog-show" command.  This
60 # probably isn't really needed...
61 #
62 screenHProp = tipArg = nil;
63 INIT = func {
64     screenHProp = props.globals.getNode("/sim/startup/ysize");
65     tipArg = props.Node.new({ "dialog-name" : "PopTip" });
66
67     props.globals.getNode("/sim/help/debug", 1).setValues(debug_keys);
68     props.globals.getNode("/sim/help/basic", 1).setValues(basic_keys);
69     props.globals.getNode("/sim/help/common", 1).setValues(common_aircraft_keys);
70
71     # enable/disable menu entries
72     menuEnable("fuel-and-payload", getprop("/sim/flight-model") == "yasim");
73     menuEnable("autopilot", props.globals.getNode("/autopilot/KAP140/locks") == nil);
74     menuEnable("tutorial-start", size(props.globals.getNode("/sim/tutorials").getChildren("tutorial")));
75     menuEnable("joystick-info", size(props.globals.getNode("/input/joysticks").getChildren("js")));
76
77     var fps = props.globals.getNode("/sim/rendering/fps-display", 1);
78     setlistener(fps, fpsDisplay, 1);
79     setlistener("/sim/startup/xsize",
80         func { if (fps.getValue()) { fpsDisplay(0); fpsDisplay(1) } });
81 }
82 settimer(INIT, 1);
83
84
85 ##
86 # Show/hide the fps display dialog.
87 #
88 fpsDisplay = func {
89     var w = (caller(0)[0]["arg"] == nil) ? cmdarg().getBoolValue() : arg[0];
90     fgcommand(w ? "dialog-show" : "dialog-close", props.Node.new({"dialog-name": "fps"}));
91 }
92
93
94 ##
95 # How many seconds do we show the tip?
96 #
97 DELAY = 1.0;
98
99 ##
100 # Pop down the tip dialog, if it is visible.
101 #
102 popdown = func { fgcommand("dialog-close", tipArg); }
103
104 # Marker for the "current" timer.  This value gets stored in the
105 # closure of the timer function, and is used to check that there
106 # hasn't been a more recent timer set that should override.
107 currTimer = 0;
108
109 ########################################################################
110 # Widgets & Layout Management
111 ########################################################################
112
113 ##
114 # A "widget" class that wraps a property node.  It provides useful
115 # helper methods that are difficult or tedious with the raw property
116 # API.  Note especially the slightly tricky addChild() method.
117 #
118 Widget = {
119     set : func { me.node.getNode(arg[0], 1).setValue(arg[1]); },
120     prop : func { return me.node; },
121     new : func { return { parents : [Widget], node : props.Node.new() } },
122     addChild : func {
123         type = arg[0];
124         idx = size(me.node.getChildren(type));
125         name = type ~ "[" ~ idx ~ "]";
126         newnode = me.node.getNode(name, 1);
127         return { parents : [Widget], node : newnode };
128     },
129     setColor : func(r, g, b, a = 1) {
130         me.node.setValues({ color : { red:r, green:g, blue:b, alpha:a } });
131     },
132     setFont : func(n, s = 13, t = 0) {
133         me.node.setValues({ font : { name:n, "size":s, slant:t } });
134     },
135     setBinding : func(cmd, carg = nil) {
136         var idx = size(me.node.getChildren("binding"));
137         var node = me.node.getChild("binding", idx, 1);
138         node.getNode("command", 1).setValue(cmd);
139         if (cmd == "nasal") {
140             node.getNode("script", 1).setValue(carg);
141         } elsif (carg != nil and (cmd == "dialog-apply" or cmd == "dialog-update")) {
142             node.getNode("object-name", 1).setValue(carg);
143         }
144     },
145 };
146
147
148
149 ##
150 # Dialog class. Maintains one XML dialog.
151 #
152 # SYNOPSIS:
153 # (B) Dialog.new(<dialog-name>);   ... use dialog from $FG_ROOT/gui/dialogs/
154 #
155 # (A) Dialog.new(<prop>, <path> [, <dialog-name>]);
156 #                                  ... load aircraft specific dialog from
157 #                                      <path> under property <prop> and under
158 #                                      name <dialog-name>; if no name is given,
159 #                                      then it's taken from the XML dialog
160 #
161 #         prop        ... target node (name must be "dialog")
162 #         path        ... file path relative to $FG_ROOT
163 #         dialog-name ... dialog <name> of dialog in $FG_ROOT/gui/dialogs/
164 #
165 # EXAMPLES:
166 #
167 #     var dlg = gui.Dialog.new("/sim/gui/dialogs/foo-config/dialog",
168 #                              "Aircraft/foo/foo_config.xml");
169 #     dlg.open();
170 #     dlg.close();
171 #
172 #     var livery_dialog = gui.Dialog.new("livery-select");
173 #     livery_dialog.toggle();
174 #
175 Dialog = {
176     new : func(prop, path = nil, name = nil) {
177         var m = { parents : [Dialog] };
178         m.state = 0;
179         if (path == nil) { # global dialog in $FG_ROOT/gui/dialogs/
180             m.name = prop;
181             m.prop = props.Node.new({ "dialog-name" : prop });
182         } else {           # aircraft dialog with given path
183             m.name = name;
184             m.path = path;
185             m.prop = isa(prop, props.Node) ? prop : props.globals.getNode(prop, 1);
186             if (m.prop.getName() != "dialog")
187                 die("Dialog class: node name must end with '/dialog'");
188
189             m.listener = setlistener("/sim/signals/reinit-gui", func { m.load() }, 1);
190         }
191         return Dialog.instance[m.name] = m;
192     },
193     # doesn't need to be called explicitly, but can be used to force a reload
194     load : func {
195         var state = me.state;
196         if (state)
197             me.close();
198
199         me.prop.removeChildren();
200         fgcommand("loadxml", props.Node.new({"filename": getprop("/sim/fg-root") ~ "/" ~ me.path,
201                 "targetnode": me.prop.getPath()}));
202
203         var n = me.prop.getNode("name");
204         if (n == nil)
205             die("Dialog class: XML dialog must have <name>");
206
207         if (me.name == nil)
208             me.name = n.getValue();
209         else
210             n.setValue(me.name);
211
212         me.prop.getNode("dialog-name", 1).setValue(me.name);
213         fgcommand("dialog-new", me.prop);
214         if (state)
215             me.open();
216     },
217     # allows access to dialog-embedded Nasal variables/functions
218     namespace : func {
219         var ns = "__dlg:" ~ me.name;
220         me.state and contains(globals, ns) ? globals[ns] : nil;
221     },
222     open : func {
223         fgcommand("dialog-show", me.prop);
224         me.state = 1;
225     },
226     close : func {
227         fgcommand("dialog-close", me.prop);
228         me.state = 0;
229     },
230     toggle : func {
231         me.state ? me.close() : me.open();
232     },
233     is_open : func {
234         me.state;
235     },
236     instance : {},
237 };
238
239
240 ##
241 # FileSelector class (derived from Dialog class).
242 #
243 # SYNOPSIS: FileSelector.new(<callback>, <title>, <button> [, <pattern> [, <dir> [, <file> [, <dotfiles>]]]])
244 #
245 #         callback ... callback function that gets return value as cmdarg().getValue()
246 #         title    ... dialog title
247 #         button   ... button text (should say "Save", "Load", etc. and not just "OK")
248 #         pattern  ... array with shell pattern or nil (which is equivalent to "*")
249 #         dir      ... starting dir ($FG_ROOT if unset)
250 #         file     ... pre-selected default file name
251 #         dotfiles ... flag that decides whether UNIX dotfiles should be shown (1) or not (0)
252 #
253 # EXAMPLE:
254 #
255 #     var report = func { print("file ", cmdarg().getValue(), " selected") }
256 #     var selector = gui.FileSelector.new(
257 #             report,                 # callback function
258 #             "Save Flight",          # dialog title
259 #             "Save",                 # button text
260 #             ["*.sav", "*.xml"],     # pattern for displayed files
261 #             "/tmp",                 # start dir
262 #             "flight.sav");          # default file name
263 #     selector.open();
264 #
265 #     selector.close();
266 #     selector.set_title("Save Another Flight");
267 #     selector.open();
268 #
269 var FileSelector = {
270     new : func(callback, title, button, pattern = nil, dir = "", file = "", dotfiles = 0) {
271         var name = "file-select-";
272         var data = props.globals.getNode("/sim/gui/dialogs/", 1);
273         var i = nil;
274         for (i = 1; 1; i += 1)
275             if (data.getNode(name ~ i, 0) == nil)
276                 break;
277         data = data.getNode(name ~= i, 1);
278
279         var m = Dialog.new(data.getNode("dialog", 1), "gui/dialogs/file-select.xml", name);
280         m.parents = [FileSelector, Dialog];
281         m.data = data;
282         m.set_title(title);
283         m.set_button(button);
284         m.set_directory(dir);
285         m.set_file(file);
286         m.set_dotfiles(dotfiles);
287         m.set_pattern(pattern);
288         m.cblistener = setlistener(data.getNode("path", 1), callback);
289         return m;
290     },
291     # setters only take effect after the next call to open()
292     set_title     : func(title) { me.data.getNode("title", 1).setValue(title) },
293     set_button    : func(button) { me.data.getNode("button", 1).setValue(button) },
294     set_directory : func(dir) { me.data.getNode("directory", 1).setValue(dir) },
295     set_file      : func(file) { me.data.getNode("selection", 1).setValue(file) },
296     set_dotfiles  : func(dot) { me.data.getNode("dotfiles", 1).setBoolValue(dot) },
297     set_pattern   : func(pattern) {
298         me.data.removeChildren("pattern");
299         if (pattern != nil)
300             forindex (var i; pattern)
301                 me.data.getChild("pattern", i, 1).setValue(pattern[i]);
302     },
303     del : func {
304         me.close();
305         delete(me.instance, me.name);
306         removelistener(me.cblistener);
307         props.globals.getNode("/sim/gui/dialogs", 1).removeChildren(me.name);
308     },
309 };
310
311
312 ##
313 # Open property browser with given target path.
314 #
315 var property_browser = func(dir = "/") {
316     var dlgname = "property-browser";
317     foreach (var module; keys(globals)) {
318         if (find("__dlg:" ~ dlgname, module) == 0) {
319             globals[module].clone(dir);
320             return;
321         }
322     }
323     setprop("/sim/gui/dialogs/" ~ dlgname ~ "/last", dir);
324     fgcommand("dialog-show", props.Node.new({"dialog-name": dlgname}));
325 }
326
327
328 ##
329 # Open one property browser per /browser[] property, where each contains
330 # the target path. On the command line use  --prop:browser=orientation
331 #
332 settimer(func {
333     foreach (var b; props.globals.getChildren("browser")) {
334         var path = b.getValue();
335         if (path != nil and size(path))
336             property_browser(path);
337     }
338     props.globals.removeChildren("browser");
339 }, 0);
340
341
342 ##
343 # Apply whole dialog or list of widgets. This copies the widgets'
344 # visible contents to the respective <property>.
345 #
346 var dialog_apply = func(dialog, objects...) {
347     var n = props.Node.new({ "dialog-name" : dialog });
348     if (!size(objects)) {
349         return fgcommand("dialog-apply", n);
350     }
351     var name = n.getNode("object-name", 1);
352     foreach (var o; objects) {
353         name.setValue(o);
354         fgcommand("dialog-apply", n);
355     }
356 }
357
358
359 ##
360 # Update whole dialog or list of widgets. This makes the widgets
361 # adopt and display the value of their <property>.
362 #
363 var dialog_update = func(dialog, objects...) {
364     var n = props.Node.new({ "dialog-name" : dialog });
365     if (!size(objects)) {
366         return fgcommand("dialog-update", n);
367     }
368     var name = n.getNode("object-name", 1);
369     foreach (var o; objects) {
370         name.setValue(o);
371         fgcommand("dialog-update", n);
372     }
373 }
374
375
376 ########################################################################
377 # GUI theming
378 ########################################################################
379
380 var nextStyle = func {
381     var curr = getprop("/sim/gui/current-style");
382     var styles = props.globals.getNode("/sim/gui").getChildren("style");
383     forindex (var i; styles)
384         if (styles[i].getIndex() == curr)
385             break;
386     if ((i += 1) >= size(styles))
387         i = 0;
388     setprop("/sim/gui/current-style", styles[i].getIndex());
389     fgcommand("gui-redraw");
390 }
391
392
393 ########################################################################
394 # Dialog Boxes
395 ########################################################################
396
397 dialog = {};
398
399 var setWeight = func(wgt, opt) {
400     var lbs = opt.getNode("lbs", 1).getValue();
401     wgt.getNode("weight-lb", 1).setValue(lbs);
402
403     # Weights can have "tank" indices which set the capacity of the
404     # corresponding tank.  This code should probably be moved to
405     # something like fuel.setTankCap(tank, gals)...
406     if(wgt.getNode("tank") == nil) { return 0; }
407     var ti = wgt.getNode("tank").getValue();
408     var gn = opt.getNode("gals");
409     var gals = gn == nil ? 0 : gn.getValue();
410     var tn = props.globals.getNode("consumables/fuel/tank["~ti~"]", 1);
411     var ppg = tn.getNode("density-ppg", 1).getValue();
412     var lbs = gals * ppg;
413     var curr = tn.getNode("level-gal_us", 1).getValue();
414     curr = curr > gals ? gals : curr;
415     tn.getNode("capacity-gal_us", 1).setValue(gals);
416     tn.getNode("level-gal_us", 1).setValue(curr);
417     tn.getNode("level-lbs", 1).setValue(curr * ppg);
418     return 1;
419 }
420
421 # Checks the /sim/weight[n]/{selected|opt} values and sets the
422 # appropriate weights therefrom.
423 var setWeightOpts = func {
424     var tankchange = 0;
425     foreach(w; props.globals.getNode("sim").getChildren("weight")) {
426         var selected = w.getNode("selected");
427         if(selected != nil) {
428             foreach(opt; w.getChildren("opt")) {
429                 if(opt.getNode("name", 1).getValue() == selected.getValue()) {
430                     if(setWeight(w, opt)) { tankchange = 1; }
431                     break;
432                 }
433             }
434         }
435     }
436     return tankchange;
437 }
438 # Run it at startup and on reset to make sure the tank settings are correct
439 _setlistener("/sim/signals/fdm-initialized", func { settimer(setWeightOpts, 0) });
440 _setlistener("/sim/signals/reset", func { cmdarg().getBoolValue() or setWeightOpts() });
441
442
443 # Called from the F&W dialog when the user selects a weight option
444 var weightChangeHandler = func {
445     var tankchanged = setWeightOpts();
446
447     # This is unfortunate.  Changing tanks means that the list of
448     # tanks selected and their slider bounds must change, but our GUI
449     # isn't dynamic in that way.  The only way to get the changes on
450     # screen is to pop it down and recreate it.
451     if(tankchanged) {
452         var p = props.Node.new({"dialog-name" : "WeightAndFuel"});
453         fgcommand("dialog-close", p);
454         showWeightDialog();
455     }
456 }
457
458 ##
459 # Dynamically generates a weight & fuel configuration dialog specific to
460 # the aircraft.
461 #
462 showWeightDialog = func {
463     name = "WeightAndFuel";
464     title = "Weight and Fuel Settings";
465
466     #
467     # General Dialog Structure
468     #
469     dialog[name] = Widget.new();
470     dialog[name].set("name", name);
471     dialog[name].set("layout", "vbox");
472
473     header = dialog[name].addChild("text");
474     header.set("label", title);
475
476     dialog[name].addChild("hrule");
477
478     if (props.globals.getNode("/yasim") == nil) {
479         msg = dialog[name].addChild("text");
480         msg.set("label", "Not supported for this aircraft");
481         cancel = dialog[name].addChild("button");
482         cancel.set("legend", "Cancel");
483         cancel.setBinding("dialog-close");
484         fgcommand("dialog-new", dialog[name].prop());
485         showDialog(name);
486         return;
487     }
488
489
490     contentArea = dialog[name].addChild("group");
491     contentArea.set("layout", "hbox");
492
493     grossWgt = props.globals.getNode("/yasim/gross-weight-lbs");
494     if(grossWgt != nil) {
495         gwg = dialog[name].addChild("group");
496         gwg.set("layout", "hbox");
497         gwg.addChild("empty").set("stretch", 1);
498         gwg.addChild("text").set("label", "Gross Weight:");
499         txt = gwg.addChild("text");
500         txt.set("label", "0123456789");
501         txt.set("format", "%.0f lb");
502         txt.set("property", "/yasim/gross-weight-lbs");
503         txt.set("live", 1);
504         gwg.addChild("empty").set("stretch", 1);
505     }
506
507     buttonBar = dialog[name].addChild("group");
508     buttonBar.set("layout", "hbox");
509     buttonBar.set("default-padding", 10);
510
511     ok = buttonBar.addChild("button");
512     ok.set("legend", "OK");
513     ok.set("key", "esc");
514     ok.setBinding("dialog-apply");
515     ok.setBinding("dialog-close");
516
517     # Temporary helper function
518     tcell = func(parent, type, row, col) {
519         cell = parent.addChild(type);
520         cell.set("row", row);
521         cell.set("col", col);
522         return cell;
523     }
524
525     #
526     # Fill in the content area
527     #
528     fuelArea = contentArea.addChild("group");
529     fuelArea.set("layout", "vbox");
530     fuelArea.addChild("text").set("label", "Fuel Tanks");
531
532     fuelTable = fuelArea.addChild("group");
533     fuelTable.set("layout", "table");
534
535     fuelArea.addChild("empty").set("stretch", 1);
536
537     tcell(fuelTable, "text", 0, 0).set("label", "Tank");
538     tcell(fuelTable, "text", 0, 3).set("label", "Pounds");
539     tcell(fuelTable, "text", 0, 4).set("label", "Gallons");
540
541     tanks = props.globals.getNode("/consumables/fuel").getChildren("tank");
542     for(i=0; i<size(tanks); i+=1) {
543         t = tanks[i];
544
545         tname = i ~ "";
546         tnode = t.getNode("name");
547         if(tnode != nil) { tname = tnode.getValue(); }
548
549         tankprop = "/consumables/fuel/tank["~i~"]";
550
551         cap = t.getNode("capacity-gal_us", 1).getValue();
552
553         # Hack, to ignore the "ghost" tanks created by the C++ code.
554         if(cap == nil or cap < 1) { continue; }
555
556         title = tcell(fuelTable, "text", i+1, 0);
557         title.set("label", tname);
558         title.set("halign", "right");
559
560         sel = tcell(fuelTable, "checkbox", i+1, 1);
561         sel.set("property", tankprop ~ "/selected");
562         sel.set("live", 1);
563         sel.setBinding("dialog-apply");
564
565         slider = tcell(fuelTable, "slider", i+1, 2);
566         slider.set("property", tankprop ~ "/level-gal_us");
567         slider.set("live", 1);
568         slider.set("min", 0);
569         slider.set("max", cap);
570         slider.setBinding("dialog-apply");
571
572         lbs = tcell(fuelTable, "text", i+1, 3);
573         lbs.set("property", tankprop ~ "/level-lbs");
574         lbs.set("label", "0123456");
575         lbs.set("format", "%.3f");
576         lbs.set("live", 1);
577
578         gals = tcell(fuelTable, "text", i+1, 4);
579         gals.set("property", tankprop ~ "/level-gal_us");
580         gals.set("label", "0123456");
581         gals.set("format", "%.3f");
582         gals.set("live", 1);
583     }
584
585     weightArea = contentArea.addChild("group");
586     weightArea.set("layout", "vbox");
587     weightArea.addChild("text").set("label", "Payload");
588
589     weightTable = weightArea.addChild("group");
590     weightTable.set("layout", "table");
591
592     weightArea.addChild("empty").set("stretch", 1);
593
594     tcell(weightTable, "text", 0, 0).set("label", "Location");
595     tcell(weightTable, "text", 0, 2).set("label", "Pounds");
596
597     wgts = props.globals.getNode("/sim").getChildren("weight");
598     for(i=0; i<size(wgts); i+=1) {
599         var w = wgts[i];
600         var wname = w.getNode("name", 1).getValue();
601         var wprop = "/sim/weight[" ~ i ~ "]";
602
603         title = tcell(weightTable, "text", i+1, 0);
604         title.set("label", wname);
605         title.set("halign", "right");
606
607         if(w.getNode("opt") != nil) {
608             var combo = tcell(weightTable, "combo", i+1, 1);
609             combo.set("property", wprop ~ "/selected");
610             combo.set("pref-width", 300);
611
612             # Simple code we'd like to use:
613             #foreach(opt; w.getChildren("opt")) {
614             #   var ent = combo.addChild("value");
615             #   ent.prop().setValue(opt.getNode("name", 1).getValue());
616             #}
617
618             # More complicated workaround to move the "current" item
619             # into the first slot, because dialog.cxx doesn't set the
620             # selected item in the combo box.
621             var opts = [];
622             var curr = w.getNode("selected");
623             curr = curr == nil ? "" : curr.getValue();
624             foreach(opt; w.getChildren("opt")) {
625                 append(opts, opt.getNode("name", 1).getValue());
626             }
627             forindex(oi; opts) {
628                 if(opts[oi] == curr) {
629                     var tmp = opts[0];
630                     opts[0] = opts[oi];
631                     opts[oi] = tmp;
632                     break;
633                 }
634             }
635             foreach(opt; opts) {
636                 combo.addChild("value").prop().setValue(opt);
637             }
638
639             combo.setBinding("dialog-apply");
640             combo.setBinding("nasal", "gui.weightChangeHandler()");
641         } else {
642             var slider = tcell(weightTable, "slider", i+1, 1);
643             slider.set("property", wprop ~ "/weight-lb");
644             var min = w.getNode("min-lb", 1).getValue();
645             var max = w.getNode("max-lb", 1).getValue();
646             slider.set("min", min != nil ? min : 0);
647             slider.set("max", max != nil ? max : 100);
648             slider.set("live", 1);
649             slider.setBinding("dialog-apply");
650         }
651
652         lbs = tcell(weightTable, "text", i+1, 2);
653         lbs.set("property", wprop ~ "/weight-lb");
654         lbs.set("label", "0123456");
655         lbs.set("format", "%.0f");
656         lbs.set("live", 1);
657     }
658
659     # All done: pop it up
660     fgcommand("dialog-new", dialog[name].prop());
661     showDialog(name);
662 }
663
664
665
666
667 ##
668 # Dynamically generates a dialog from a help node.
669 #
670 # gui.showHelpDialog([<path> [, toggle]])
671 #
672 # path   ... path to help node
673 # toggle ... decides if an already open dialog should be closed
674 #            (useful when calling the dialog from a key binding; default: 0)
675 #
676 # help node
677 # =========
678 # each of <title>, <key>, <line>, <text> is optional; uses
679 # "/sim/description" or "/sim/aircraft" if <title> is omitted;
680 # only the first <text> is displayed
681 #
682 #
683 # <help>
684 #     <title>dialog title<title>
685 #     <key>
686 #         <name>g/G</name>
687 #         <desc>gear up/down</desc>
688 #     </key>
689 #
690 #     <line>one line</line>
691 #     <line>another line</line>
692 #
693 #     <text>text in
694 #           scrollable widget
695 #     </text>
696 # </help>
697 #
698 showHelpDialog = func {
699     node = props.globals.getNode(arg[0]);
700     if (arg[0] == "/sim/help" and size(node.getChildren()) < 4) {
701         node = node.getChild("common");
702     }
703
704     name = node.getNode("title", 1).getValue();
705     if (name == nil) {
706         name = getprop("/sim/description");
707         if (name == nil) {
708             name = getprop("/sim/aircraft");
709         }
710     }
711     toggle = size(arg) > 1 and arg[1] != nil and arg[1] > 0;
712     if (toggle and contains(dialog, name)) {
713         fgcommand("dialog-close", props.Node.new({ "dialog-name" : name }));
714         delete(dialog, name);
715         return;
716     }
717
718     dialog[name] = Widget.new();
719     dialog[name].set("layout", "vbox");
720     dialog[name].set("default-padding", 0);
721     dialog[name].set("name", name);
722
723     # title bar
724     titlebar = dialog[name].addChild("group");
725     titlebar.set("layout", "hbox");
726     titlebar.addChild("empty").set("stretch", 1);
727     titlebar.addChild("text").set("label", name);
728     titlebar.addChild("empty").set("stretch", 1);
729
730     w = titlebar.addChild("button");
731     w.set("pref-width", 16);
732     w.set("pref-height", 16);
733     w.set("legend", "");
734     w.set("default", 1);
735     w.set("key", "esc");
736     w.setBinding("nasal", "delete(gui.dialog, \"" ~ name ~ "\")");
737     w.setBinding("dialog-close");
738
739     dialog[name].addChild("hrule");
740
741     # key list
742     keylist = dialog[name].addChild("group");
743     keylist.set("layout", "table");
744     keylist.set("default-padding", 2);
745     keydefs = node.getChildren("key");
746     n = size(keydefs);
747     row = col = 0;
748     foreach (key; keydefs) {
749         if (n >= 60 and row >= n / 3 or n >= 16 and row >= n / 2) {
750             col += 1;
751             row = 0;
752         }
753
754         w = keylist.addChild("text");
755         w.set("row", row);
756         w.set("col", 2 * col);
757         w.set("halign", "right");
758         w.set("label", " " ~ key.getNode("name").getValue());
759
760         w = keylist.addChild("text");
761         w.set("row", row);
762         w.set("col", 2 * col + 1);
763         w.set("halign", "left");
764         w.set("label", "... " ~ key.getNode("desc").getValue() ~ "  ");
765         row += 1;
766     }
767
768     # separate lines
769     lines = node.getChildren("line");
770     if (size(lines)) {
771         if (size(keydefs)) {
772             dialog[name].addChild("empty").set("pref-height", 4);
773             dialog[name].addChild("hrule");
774             dialog[name].addChild("empty").set("pref-height", 4);
775         }
776
777         g = dialog[name].addChild("group");
778         g.set("layout", "vbox");
779         g.set("default-padding", 1);
780         foreach (var lin; lines) {
781             foreach (var l; split("\n", lin.getValue())) {
782                 w = g.addChild("text");
783                 w.set("halign", "left");
784                 w.set("label", " " ~ l ~ " ");
785             }
786         }
787     }
788
789     # scrollable text area
790     if (node.getNode("text") != nil) {
791         dialog[name].addChild("empty").set("pref-height", 10);
792
793         width = [640, 800, 1152][col];
794         height = screenHProp.getValue() - (100 + (size(keydefs) / (col + 1) + size(lines)) * 28);
795         if (height < 200) {
796             height = 200;
797         }
798
799         w = dialog[name].addChild("textbox");
800         w.set("halign", "fill");
801         w.set("slider", 20);
802         w.set("pref-width", width);
803         w.set("pref-height", height);
804         w.set("editable", 0);
805         w.set("property", node.getPath() ~ "/text");
806     } else {
807         dialog[name].addChild("empty").set("pref-height", 8);
808     }
809     fgcommand("dialog-new", dialog[name].prop());
810     showDialog(name);
811 }
812
813
814 debug_keys = {
815     title : "Development Keys",
816     key : [
817        #{ name : "Ctrl-U",    desc : "add 1000 ft of emergency altitude" },
818         { name : "F2",        desc : "force tile cache reload" },
819         { name : "F4",        desc : "force lighting update" },
820         { name : "F8",        desc : "cycle fog type" },
821         { name : "F9",        desc : "toggle textures" },
822         { name : "Shift-F3",  desc : "load panel" },
823         { name : "Shift-F4",  desc : "reload global preferences" },
824         { name : "Shift-F9",  desc : "toggle FDM data logging" },
825         { name : "Ctrl-Space", desc : "open property browser" },
826     ],
827 };
828
829 basic_keys = {
830     title : "Basic Keys",
831     key : [
832         { name : "?",         desc : "show/hide aircraft help dialog" },
833        #{ name : "Tab",       desc : "show/hide aircraft config dialog" },
834         { name : "Esc",       desc : "quit FlightGear" },
835         { name : "Shift-Esc", desc : "reset FlightGear" },
836         { name : "a/A",       desc : "increase/decrease speed-up" },
837         { name : "c",         desc : "toggle 3D/2D cockpit" },
838         { name : "Ctrl-C",    desc : "toggle clickable panel hotspots" },
839         { name : "p",         desc : "pause/continue sim" },
840         { name : "r",         desc : "activate instant replay system" },
841         { name : "Ctrl-R",    desc : "show radio setting dialog" },
842         { name : "t/T",       desc : "increase/decrease warp delta" },
843         { name : "v/V",       desc : "cycle views (forward/backward)" },
844         { name : "Ctrl-V",    desc : "select cockpit view" },
845         { name : "w/W",       desc : "increase/decrease warp" },
846         { name : "x/X",       desc : "zoom in/out" },
847         { name : "Ctrl-X",    desc : "reset zoom to default" },
848         { name : "z/Z",       desc : "increase/decrease visibility" },
849         { name : "'",         desc : "display ATC setting dialog" },
850         { name : "+",         desc : "let ATC/instructor repeat last message" },
851         { name : "F1",        desc : "load flight" },
852         { name : "F3",        desc : "capture screen" },
853         { name : "F10",       desc : "toggle menubar" },
854         { name : "Shift-F2",  desc : "save flight" },
855         { name : "Shift-F10", desc : "cycle through GUI styles" },
856     ],
857 };
858
859 common_aircraft_keys = {
860     title : "Common Aircraft Keys",
861     key : [
862         { name : "Enter",     desc : "move rudder right" },
863         { name : "0/Insert",  desc : "move rudder left" },
864         { name : "1/End",     desc : "decrease elevator trim" },
865         { name : "2/Up",      desc : "increase elevator or AP altitude" },
866         { name : "3/PgDn",    desc : "decr. throttle or AP autothrottle" },
867         { name : "4/Left",    desc : "move aileron left or adj. AP hdg." },
868         { name : "5/KP5",     desc : "center aileron, elev., and rudder" },
869         { name : "6/Right",   desc : "move aileron right or adj. AP hdg." },
870         { name : "7/Home",    desc : "increase elevator trim" },
871         { name : "8/Down",    desc : "decrease elevator or AP altitude" },
872         { name : "9/PgUp",    desc : "incr. throttle or AP autothrottle" },
873         { name : "Space",     desc : "PTT - Push To Talk (via VoIP)" },
874         { name : "!/@/#/$",   desc : "select engine 1/2/3/4" },
875         { name : "b",         desc : "apply all brakes" },
876         { name : "B",         desc : "toggle parking brake" },
877        #{ name : "Ctrl-B",    desc : "toggle speed brake" },
878         { name : "g/G",       desc : "gear up/down" },
879         { name : "h",         desc : "cycle HUD (head up display)" },
880         { name : "H",         desc : "cycle HUD brightness" },
881         { name : "i/Shift-i", desc : "normal/minimal HUD" },
882        #{ name : "j",         desc : "decrease spoilers" },
883        #{ name : "k",         desc : "increase spoilers" },
884         { name : "l",         desc : "toggle tail-wheel lock" },
885         { name : "m/M",       desc : "mixture richer/leaner" },
886         { name : "n/N",       desc : "propeller finer/coarser" },
887         { name : "P",         desc : "toggle 2D panel" },
888         { name : "S",         desc : "swap panels" },
889         { name : "s",         desc : "fire starter on selected eng." },
890         { name : ", .",       desc : "left/right brake (comma, period)" },
891         { name : "~",         desc : "select all engines (tilde)" },
892         { name : "[ ]",       desc : "flaps up/down" },
893         { name : "{ }",       desc : "decr/incr magneto on sel. eng." },
894         { name : "Ctrl-A",    desc : "AP: toggle altitude lock" },
895         { name : "Ctrl-G",    desc : "AP: toggle glide slope lock" },
896         { name : "Ctrl-H",    desc : "AP: toggle heading lock" },
897         { name : "Ctrl-N",    desc : "AP: toggle NAV1 lock" },
898         { name : "Ctrl-P",    desc : "AP: toggle pitch hold" },
899         { name : "Ctrl-S",    desc : "AP: toggle auto-throttle" },
900         { name : "Ctrl-T",    desc : "AP: toggle terrain lock" },
901         { name : "Ctrl-W",    desc : "AP: toggle wing leveler" },
902         { name : "F6",        desc : "AP: toggle heading mode" },
903         { name : "F11",       desc : "pop up autopilot (AP) dialog" },
904         { name : "Shift-F5",  desc : "scroll 2D panel down" },
905         { name : "Shift-F6",  desc : "scroll 2D panel up" },
906         { name : "Shift-F7",  desc : "scroll 2D panel left" },
907         { name : "Shift-F8",  desc : "scroll 2D panel right" },
908     ],
909 };