FGCom: add dialog for FGCom
[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.  The third argument can be a hash with override values.
5 # Note that the tip dialog is a shared resource.  If someone else
6 # comes along and wants to pop a tip up before your delay is finished,
7 # you lose. :)
8 #
9 var popupTip = func(label, delay = nil, override = nil)
10 {
11     fgcommand("show-message", props.Node.new({ "label": label, "delay":delay }));
12
13
14 }
15
16 var showDialog = func(name) {
17     fgcommand("dialog-show", props.Node.new({ "dialog-name" : name }));
18 }
19
20 ##
21 # Enable/disable named menu entry
22 #
23 var menuEnable = func(searchname, state) {
24     foreach (var menu; props.globals.getNode("/sim/menubar/default").getChildren("menu")) {
25         foreach (var name; menu.getChildren("name")) {
26             if (name.getValue() == searchname) {
27                 menu.getNode("enabled").setBoolValue(state);
28             }
29         }
30         foreach (var item; menu.getChildren("item")) {
31             foreach (var name; item.getChildren("name")) {
32                 if (name.getValue() == searchname) {
33                     item.getNode("enabled").setBoolValue(state);
34                 }
35             }
36         }
37     }
38 }
39
40 ##
41 # Set the binding for a menu item to a Nasal script,
42 # typically a dialog open() command.
43 #
44 var menuBind = func(searchname, command) {
45     foreach (var menu; props.globals.getNode("/sim/menubar/default").getChildren("menu")) {
46         foreach (var item; menu.getChildren("item")) {
47             foreach (var name; item.getChildren("name")) {
48                 if (name.getValue() == searchname) {
49                     item.getNode("binding", 1).getNode("command", 1).setValue("nasal");
50                     item.getNode("binding", 1).getNode("script", 1).setValue(command);
51                     fgcommand("gui-redraw");
52                 }
53             }
54         }
55     }
56 }
57
58 ##
59 # Set mouse cursor coordinates and shape (number or name), and return
60 # current shape (number).
61 #
62 # Example:  var cursor = gui.setCursor();
63 #           gui.setCursor(nil, nil, "wait");
64 #
65 var setCursor = func(x = nil, y = nil, cursor = nil) {
66     var args = props.Node.new();
67     if (x != nil) args.getNode("x", 1).setIntValue(x);
68     if (y != nil) args.getNode("y", 1).setIntValue(y);
69     if (cursor != nil) {
70         if (num(cursor) == nil)
71             cursor = cursor_types[cursor];
72         if (cursor == nil)
73             die("cursor must be one of: " ~ string.join(", ", keys(cursor_types)));
74         setprop("/sim/mouse/hide-cursor", cursor);
75         args.getNode("cursor", 1).setIntValue(cursor);
76     }
77     fgcommand("set-cursor", args);
78     return args.getNode("cursor").getValue();
79 }
80
81 ##
82 # Supported mouse cursor types.
83 #
84 var cursor_types = { none: 0, pointer: 1, wait: 2, crosshair: 3, leftright: 4,
85     topside: 5, bottomside: 6, leftside: 7, rightside: 8,
86     topleft: 9, topright: 10, bottomleft: 11, bottomright: 12,
87 };
88
89 ##
90 # Find a GUI element by given name.
91 # dialog: dialog root property.
92 # name: name of GUI element to be searched.
93 # Returns GUI element when found, nil otherwise.
94 #
95 var findElementByName = func(dialog,name) {
96         foreach( var child; dialog.getChildren() ) {
97                 var n = child.getNode( "name" );
98                 if( n != nil and n.getValue() == name )
99                         return child;
100                 var f = findElementByName(child, name);
101                 if( f != nil ) return f;
102         }
103         return nil;
104 };
105
106
107 ########################################################################
108 # Private Stuff:
109 ########################################################################
110
111 ##
112 # Initialize property nodes via a timer, to insure the props module is
113 # loaded.  See notes in view.nas.  Simply cache the screen height
114 # property and the argument for the "dialog-show" command.  This
115 # probably isn't really needed...
116 #
117 var fdm = getprop("/sim/flight-model");
118 var screenHProp = nil;
119 var tipArg = nil;
120 var autopilotDisableProps = [
121   "/autopilot/hide-menu",
122   "/autopilot/KAP140/locks",
123   "/autopilot/CENTURYIIB/locks",
124   "/autopilot/CENTURYIII/locks"
125 ];
126
127 _setlistener("/sim/signals/nasal-dir-initialized", func {
128     screenHProp = props.globals.getNode("/sim/startup/ysize");
129     tipArg = props.Node.new({ "dialog-name" : "PopTip" });
130
131     props.globals.getNode("/sim/help/debug", 1).setValues(debug_keys);
132     props.globals.getNode("/sim/help/basic", 1).setValues(basic_keys);
133     props.globals.getNode("/sim/help/common", 1).setValues(common_aircraft_keys);
134
135     # enable/disable menu entries
136     menuEnable("fuel-and-payload", fdm == "yasim" or fdm == "jsb");
137     menuEnable("aircraft-checklists", props.globals.getNode("/sim/checklists") != nil);
138     var isAutopilotMenuEnabled = func {
139       foreach( var apdp; autopilotDisableProps ) {
140         if( props.globals.getNode( apdp ) != nil )
141           return 0;
142       }
143       return 1;
144     }
145     menuEnable("autopilot", isAutopilotMenuEnabled() );
146     menuEnable("joystick-info", size(props.globals.getNode("/input/joysticks").getChildren("js")));
147     menuEnable("rendering-buffers", getprop("/sim/rendering/rembrandt/enabled"));
148     menuEnable("rembrandt-buffers-choice", getprop("/sim/rendering/rembrandt/enabled"));
149     menuEnable("stereoscopic-options", !getprop("/sim/rendering/rembrandt/enabled"));
150     menuEnable("sound-config", getprop("/sim/sound/working"));
151     menuEnable("fgcom-settings", getprop("/sim/fgcom/enabled"));
152
153     # frame-per-second display
154     var fps = props.globals.getNode("/sim/rendering/fps-display", 1);
155     setlistener(fps, fpsDisplay, 1);
156     setlistener("/sim/startup/xsize", func {
157         if (fps.getValue()) {
158             fpsDisplay(0);
159             fpsDisplay(1);
160         }
161     });
162
163     # frame-latency display
164     var latency = props.globals.getNode("/sim/rendering/frame-latency-display", 1);
165     setlistener(latency, latencyDisplay, 1);
166     setlistener("/sim/startup/xsize", func {
167         if (latency.getValue()) {
168             latencyDisplay(0);
169             latencyDisplay(1);
170         }
171     });
172
173     # only enable precipitation if gui *and* aircraft want it
174     var p = "/sim/rendering/precipitation-";
175     var precip_gui = getprop(p ~ "gui-enable");
176     var precip_ac = getprop(p ~ "aircraft-enable");
177     props.globals.getNode(p ~ "enable").setAttribute("userarchive", 0); # TODO remove later
178     var set_precip = func setprop(p ~ "enable", precip_gui and precip_ac);
179     setlistener(p ~ "gui-enable", func(n) set_precip(precip_gui = n.getValue()),1);
180     setlistener(p ~ "aircraft-enable", func(n) set_precip(precip_ac = n.getValue()),1);
181
182     # the autovisibility feature of the menubar
183     # automatically show the menubar if the mouse is at the upper edge of the window
184     # the menubar is hidden by a binding to a LMB click in mode 0 in mice.xml
185     var menubarAutoVisibilityListener = nil;
186     var menubarAutoVisibilityEdge = props.globals.initNode( "/sim/menubar/autovisibility/edge-size", 5, "INT" );
187     var menubarVisibility = props.globals.initNode( "/sim/menubar/visibility", 0, "BOOL" );
188     var currentMenubarVisibility = menubarVisibility.getValue();
189     var mouseMode = props.globals.initNode( "/devices/status/mice/mouse/mode", 0, "INT" );
190
191     setlistener( "/sim/menubar/autovisibility/enabled", func(n) {
192       if( n.getValue() and menubarAutoVisibilityListener == nil ) {
193         currentMenubarVisibility = menubarVisibility.getValue();
194         menubarVisibility.setBoolValue( 0 );
195         menubarAutoVisibilityListener = setlistener( "/devices/status/mice/mouse/y", func(n) {
196           if( n.getValue() == nil ) return;
197           if( mouseMode.getValue() != 0 ) return;
198
199           if(  n.getValue() <= menubarAutoVisibilityEdge.getValue() )
200             menubarVisibility.setBoolValue( 1 );
201
202         }, 1, 0 );
203       }
204
205       # don't listen to the mouse position if this feature is enabled
206       if( n.getValue() == 0 and menubarAutoVisibilityListener != nil ) {
207         removelistener( menubarAutoVisibilityListener );
208         menubarAutoVisibilityListener = nil;
209         menubarVisibility.setBoolValue(currentMenubarVisibility);
210       }
211   }, 1, 0);
212
213 });
214
215
216 ##
217 # Show/hide the fps display dialog.
218 #
219 var fpsDisplay = func(n) {
220     var w = isa(n, props.Node) ? n.getValue() : n;
221     fgcommand(w ? "dialog-show" : "dialog-close", props.Node.new({"dialog-name": "fps"}));
222 }
223 var latencyDisplay = func(n) {
224     var w = isa(n, props.Node) ? n.getValue() : n;
225     fgcommand(w ? "dialog-show" : "dialog-close", props.Node.new({"dialog-name": "frame-latency"}));
226 }
227
228 ##
229 # How many seconds do we show the tip?
230 #
231 var DELAY = 1.0;
232
233 ##
234 # Pop down the tip dialog, if it is visible.
235 #
236 var popdown = func { fgcommand("dialog-close", tipArg); }
237
238 # Marker for the "current" timer.  This value gets stored in the
239 # closure of the timer function, and is used to check that there
240 # hasn't been a more recent timer set that should override.
241 var currTimer = 0;
242
243 ########################################################################
244 # Widgets & Layout Management
245 ########################################################################
246
247 ##
248 # A "widget" class that wraps a property node.  It provides useful
249 # helper methods that are difficult or tedious with the raw property
250 # API.  Note especially the slightly tricky addChild() method.
251 #
252 var Widget = {
253     set : func { me.node.getNode(arg[0], 1).setValue(arg[1]); },
254     prop : func { return me.node; },
255     new : func { return { parents : [Widget], node : props.Node.new() } },
256     addChild : func {
257         var type = arg[0];
258         var idx = size(me.node.getChildren(type));
259         var name = type ~ "[" ~ idx ~ "]";
260         var newnode = me.node.getNode(name, 1);
261         return { parents : [Widget], node : newnode };
262     },
263     setColor : func(r, g, b, a = 1) {
264         me.node.setValues({ color : { red:r, green:g, blue:b, alpha:a } });
265     },
266     setFont : func(n, s = 13, t = 0) {
267         me.node.setValues({ font : { name:n, "size":s, slant:t } });
268     },
269     setBinding : func(cmd, carg = nil) {
270         var idx = size(me.node.getChildren("binding"));
271         var node = me.node.getChild("binding", idx, 1);
272         node.getNode("command", 1).setValue(cmd);
273         if (cmd == "nasal") {
274             node.getNode("script", 1).setValue(carg);
275         } elsif (carg != nil and (cmd == "dialog-apply" or cmd == "dialog-update")) {
276             node.getNode("object-name", 1).setValue(carg);
277         }
278     },
279 };
280
281
282 ##
283 # Dialog class. Maintains one XML dialog.
284 #
285 # SYNOPSIS:
286 # (B) Dialog.new(<dialog-name>);   ... use dialog from $FG_ROOT/gui/dialogs/
287 #
288 # (A) Dialog.new(<prop>, <path> [, <dialog-name>]);
289 #                                  ... load aircraft specific dialog from
290 #                                      <path> under property <prop> and under
291 #                                      name <dialog-name>; if no name is given,
292 #                                      then it's taken from the XML dialog
293 #
294 #         prop        ... target node (name must be "dialog")
295 #         path        ... file path relative to $FG_ROOT
296 #         dialog-name ... dialog <name> of dialog in $FG_ROOT/gui/dialogs/
297 #
298 # EXAMPLES:
299 #
300 #     var dlg = gui.Dialog.new("/sim/gui/dialogs/foo-config/dialog",
301 #                              "Aircraft/foo/foo_config.xml");
302 #     dlg.open();
303 #     dlg.close();
304 #
305 #     var livery_dialog = gui.Dialog.new("livery-select");
306 #     livery_dialog.toggle();
307 #
308 var Dialog = {
309     new: func(prop, path = nil, name = nil) {
310         var m = { parents: [Dialog] };
311         m.state = 0;
312         m.listener = nil;
313         if (path == nil) { # global dialog in $FG_ROOT/gui/dialogs/
314             m.name = prop;
315             m.prop = props.Node.new({ "dialog-name" : prop });
316         } else {           # aircraft dialog with given path
317             m.name = name;
318             m.path = path;
319             m.prop = isa(prop, props.Node) ? prop : props.globals.getNode(prop, 1);
320             if (m.prop.getName() != "dialog")
321                 die("Dialog class: node name must end with '/dialog'");
322
323             m.listener = setlistener("/sim/signals/reinit-gui", func m.load(), 1);
324         }
325         return Dialog.instance[m.name] = m;
326     },
327     del: func
328     {
329         if (me.listener != nil)
330             removelistener(me.listener);
331     },
332     # doesn't need to be called explicitly, but can be used to force a reload
333     load: func {
334         var state = me.state;
335         if (state)
336             me.close();
337
338         me.prop.removeChildren();
339         io.read_properties(me.path, me.prop);
340
341         var n = me.prop.getNode("name");
342         if (n == nil)
343             die("Dialog class: XML dialog must have <name>");
344
345         if (me.name == nil)
346             me.name = n.getValue();
347         else
348             n.setValue(me.name);
349
350         me.prop.getNode("dialog-name", 1).setValue(me.name);
351         fgcommand("dialog-new", me.prop);
352         if (state)
353             me.open();
354     },
355     # allows access to dialog-embedded Nasal variables/functions
356     namespace: func {
357         var ns = "__dlg:" ~ me.name;
358         me.state and contains(globals, ns) ? globals[ns] : nil;
359     },
360     open: func {
361         fgcommand("dialog-show", me.prop);
362         me.state = 1;
363     },
364     close: func {
365         fgcommand("dialog-close", me.prop);
366         me.state = 0;
367     },
368     toggle: func {
369         me.state ? me.close() : me.open();
370     },
371     is_open: func {
372         me.state;
373     },
374     instance: {},
375 };
376
377
378 ##
379 # Overlay selector. Displays a list of overlay XML files and copies the
380 # chosen one to the property tree. The class allows to select liveries,
381 # insignia, decals, variants, etc. Usually the overlay properties are
382 # fed to "select" and "material" animations.
383 #
384 # SYNOPSIS:
385 #       OverlaySelector.new(<title>, <dir>, <nameprop> [, <sortprop> [, <mpprop> [, <callback>]]]);
386 #
387 #       title    ... dialog title
388 #       dir      ... directory where to find the XML overlay files,
389 #                    relative to FG_ROOT
390 #       nameprop ... property in an overlay file that contains the name
391 #                    The result is written to this place in the
392 #                    property tree.
393 #       sortprop ... property in an overlay file that should be used
394 #                    as sorting criterion, if alphabetic sorting by
395 #                    name is undesirable. Use nil if you don't need
396 #                    this, but want to set a callback function.
397 #       mpprop   ... property path of MP node where the file name should
398 #                    be written to
399 #       callback ... function that's called after a new entry was chosen,
400 #                    with these arguments:
401 #
402 #                    callback(<number>, <name>, <sort-criterion>, <file>,  <path>)
403 #
404 # EXAMPLE:
405 #       aircraft.data.add("sim/model/pilot");  # autosave the pilot
406 #       var pilots_dialog = gui.OverlaySelector.new("Pilots",
407 #               "Aircraft/foo/Models/Pilots",
408 #               "sim/model/pilot");
409 #
410 #       pilots_dialog.open();  # or ... close(), or toggle()
411 #
412 #
413 var OverlaySelector = {
414     new: func(title, dir, nameprop, sortprop = nil, mpprop = nil, callback = nil) {
415         var name = "overlay-select-";
416         var data = props.globals.getNode("/sim/gui/dialogs/", 1);
417         for (var i = 1; 1; i += 1)
418             if (data.getNode(name ~ i, 0) == nil)
419                 break;
420         data = data.getNode(name ~= i, 1);
421
422         var m = Dialog.new(data.getNode("dialog", 1), "gui/dialogs/overlay-select.xml", name);
423         m.parents = [OverlaySelector, Dialog];
424
425         # resolve the path in FG_ROOT, and --fg-aircraft dir, etc
426         m.dir = resolvepath(dir) ~ "/";
427
428         var relpath = func(p) substr(p, p[0] == `/`);
429         m.nameprop = relpath(nameprop);
430         m.sortprop = relpath(sortprop or nameprop);
431         m.mpprop = mpprop;
432         m.callback = callback;
433         m.title = title;
434         m.dialog_name = name;
435         m.result = data.initNode("result", "");
436         m.listener = setlistener(m.result, func(n) m.select(n.getValue()));
437         if (m.mpprop != nil)
438             aircraft.data.add(m.nameprop);
439         m.reinit();
440         # need to reinit again, whenever the GUI is reloaded
441         m.reinit_listener = setlistener("/sim/signals/reinit-gui", func(n) m.reinit());
442         return m;
443     },
444     reinit: func {
445         me.prop.getNode("group/text/label").setValue(me.title);
446         me.prop.getNode("group/button/binding/script").setValue('gui.Dialog.instance["' ~ me.dialog_name ~ '"].close()');
447         me.list = me.prop.getNode("list");
448         me.list.getNode("property").setValue(me.result.getPath());
449         me.rescan();
450         me.current = -1;
451         me.select(getprop(me.nameprop) or "");
452     },
453     del: func {
454         removelistener(me.listener);
455         removelistener(me.reinit_listener);
456         # call inherited 'del'
457         me.parents = subvec(me.parents,1);
458         me.del();
459     },
460     rescan: func {
461         me.data = [];
462         var files = directory(me.dir);
463         if (size(files)) {
464             foreach (var file; files) {
465                 if (substr(file, -4) != ".xml")
466                     continue;
467                 var n = io.read_properties(me.dir ~ file);
468                 var name = n.getNode(me.nameprop, 1).getValue();
469                 var index = n.getNode(me.sortprop, 1).getValue();
470                 if (name == nil or index == nil)
471                     continue;
472                 append(me.data, [name, index, substr(file, 0, size(file) - 4), me.dir ~ file]);
473             }
474             me.data = sort(me.data, func(a, b) num(a[1]) == nil or num(b[1]) == nil
475                     ? cmp(a[1], b[1]) : a[1] - b[1]);
476         }
477
478         me.list.removeChildren("value");
479         forindex (var i; me.data)
480             me.list.getChild("value", i, 1).setValue(me.data[i][0]);
481     },
482     set: func(index) {
483         var last = me.current;
484         me.current = math.mod(index, size(me.data));
485         io.read_properties(me.data[me.current][3], props.globals);
486         if (last != me.current and me.callback != nil)
487             call(me.callback, [me.current] ~ me.data[me.current], me);
488         if (me.mpprop != nil)
489             setprop(me.mpprop, me.data[me.current][2]);
490     },
491     select: func(name) {
492         forindex (var i; me.data)
493             if (me.data[i][0] == name)
494                 me.set(i);
495     },
496     next: func {
497         me.set(me.current + 1);
498     },
499     previous: func {
500         me.set(me.current - 1);
501     },
502 };
503
504
505 ##
506 # FileSelector class (derived from Dialog class).
507 #
508 # SYNOPSIS: FileSelector.new(<callback>, <title>, <button> [, <pattern> [, <dir> [, <file> [, <dotfiles>]]]])
509 #
510 #         callback ... callback function that gets return value as first argument
511 #         title    ... dialog title
512 #         button   ... button text (should say "Save", "Load", etc. and not just "OK")
513 #         pattern  ... array with shell pattern or nil (which is equivalent to "*")
514 #         dir      ... starting dir ($FG_ROOT if unset)
515 #         file     ... pre-selected default file name
516 #         dotfiles ... flag that decides whether UNIX dotfiles should be shown (1) or not (0)
517 #
518 # EXAMPLE:
519 #
520 #     var report = func(n) { print("file ", n.getValue(), " selected") }
521 #     var selector = gui.FileSelector.new(
522 #             report,                 # callback function
523 #             "Save Flight",          # dialog title
524 #             "Save",                 # button text
525 #             ["*.sav", "*.xml"],     # pattern for displayed files
526 #             "/tmp",                 # start dir
527 #             "flight.sav");          # default file name
528 #     selector.open();
529 #
530 #     selector.close();
531 #     selector.set_title("Save Another Flight");
532 #     selector.open();
533 #
534 var FileSelector = {
535     new: func(callback, title, button, pattern = nil, dir = "", file = "", dotfiles = 0, show_files=1) {
536         
537         
538         var usage = gui.FILE_DIALOG_OPEN_FILE;
539         if (!show_files) {
540             usage = gui.FILE_DIALOG_CHOOSE_DIR;
541         } else if (button == 'Save') {
542             # nasty, should make this explicit
543             usage = gui.FILE_DIALOG_SAVE_FILE;
544         }
545         
546         m = { parents:[FileSelector],
547              _inner: gui._createFileDialog(usage)};
548         
549         m.set_title(title);
550         m.set_button(button);
551         m.set_directory(dir);
552         m.set_file(file);
553         m.set_dotfiles(dotfiles);
554         m.set_pattern(pattern);
555         
556         m._inner.setCallback(func (path) {  
557             var node = props.Node.new();
558             node.setValue(path);
559             callback(node); 
560         }   );
561         
562         return m;
563     },
564     # setters only take effect after the next call to open()
565     set_title: func(title) { me._inner.title = title },
566     set_button: func(button) { me._inner.button = button },
567     set_directory: func(dir) { me._inner.directory = directory },
568     set_file: func(file) { me._inner.placeholder = file },
569     set_dotfiles: func(dot) { me._inner.show_hidden = dot },
570     set_pattern: func(pattern) { me._inner.pattern = (pattern == nil) ? [] : pattern },
571     
572     open: func() { me._inner.open(); },
573     close: func() { me._inner.close(); },
574     
575     del: func {
576         me._inner.close();
577         me._inner = nil;
578     },
579 };
580
581 ##
582 # DirSelector - convenience "class" (indeed using a reconfigured FileSelector)
583 #
584 var DirSelector = {
585   new: func(callback, title, button, dir = "") {
586      return FileSelector.new(callback, title, button, nil, dir, "", 0, 0);
587   }
588 };
589
590 ##
591 # Save/load flight menu functions.
592 #
593 var save_flight_sel = nil;
594 var save_flight = func {
595     foreach (var n; props.globals.getNode("/sim/presets").getChildren())
596         n.setAttribute("archive", 1);
597     var save = func(n) fgcommand("save", props.Node.new({ file: n.getValue() }));
598     if (save_flight_sel == nil)
599         save_flight_sel = FileSelector.new(save, "Save Flight", "Save",
600                 ["*.sav"], getprop("/sim/fg-home"), "flight.sav");
601     save_flight_sel.open();
602 }
603
604
605 var load_flight_sel = nil;
606 var load_flight = func {
607     var load = func(n) {
608         fgcommand("load", props.Node.new({ file: n.getValue() }));
609         fgcommand("presets-commit");
610     }
611     if (load_flight_sel == nil)
612         load_flight_sel = FileSelector.new(load, "Load Flight", "Load",
613                 ["*.sav"], getprop("/sim/fg-home"), "flight.sav");
614     load_flight_sel.open();
615 }
616
617 ##
618 # Screen-shot directory menu function
619 #
620 var set_screenshotdir_sel = nil;
621 var set_screenshotdir = func {
622     if (set_screenshotdir_sel == nil)
623         set_screenshotdir_sel = gui.DirSelector.new(
624             func(result) { setprop("/sim/paths/screenshot-dir", result.getValue()); },
625             "Select Screenshot Directory", "Ok", getprop("/sim/paths/screenshot-dir"));
626     set_screenshotdir_sel.open();
627 }
628
629 ##
630 # Open property browser with given target path.
631 #
632 var property_browser = func(dir = nil) {
633     if (dir == nil)
634         dir = "/";
635     elsif (isa(dir, props.Node))
636         dir = dir.getPath();
637     var dlgname = "property-browser";
638     foreach (var module; keys(globals))
639         if (find("__dlg:" ~ dlgname, module) == 0)
640             return globals[module].clone(dir);
641
642     setprop("/sim/gui/dialogs/" ~ dlgname ~ "/last", dir);
643     fgcommand("dialog-show", props.Node.new({"dialog-name": dlgname}));
644 }
645
646
647 ##
648 # Open one property browser per /browser[] property, where each contains
649 # the target path. On the command line use  --prop:browser=orientation
650 #
651 settimer(func {
652     foreach (var b; props.globals.getChildren("browser"))
653         if ((var browser = b.getValue()) != nil)
654             foreach (var path; split(",", browser))
655                 if (size(path))
656                     property_browser(string.trim(path));
657
658     props.globals.removeChildren("browser");
659 }, 0);
660
661
662 ##
663 # Apply whole dialog or list of widgets. This copies the widgets'
664 # visible contents to the respective <property>.
665 #
666 var dialog_apply = func(dialog, objects...) {
667     var n = props.Node.new({ "dialog-name": dialog });
668     if (!size(objects))
669         return fgcommand("dialog-apply", n);
670
671     var name = n.getNode("object-name", 1);
672     foreach (var o; objects) {
673         name.setValue(o);
674         fgcommand("dialog-apply", n);
675     }
676 }
677
678
679 ##
680 # Update whole dialog or list of widgets. This makes the widgets
681 # adopt and display the value of their <property>.
682 #
683 var dialog_update = func(dialog, objects...) {
684     var n = props.Node.new({ "dialog-name": dialog });
685     if (!size(objects))
686         return fgcommand("dialog-update", n);
687
688     var name = n.getNode("object-name", 1);
689     foreach (var o; objects) {
690         name.setValue(o);
691         fgcommand("dialog-update", n);
692     }
693 }
694
695
696 ##
697 # Searches a dialog tree for widgets with a particular <name> entry and
698 # sets their <enabled> flag.
699 #
700 var enable_widgets = func(node, name, enable = 1) {
701     foreach (var n; node.getChildren())
702         enable_widgets(n, name, enable);
703     if ((var n = node.getNode("name")) != nil and n.getValue() == name)
704         node.getNode("enabled", 1).setBoolValue(enable);
705 }
706
707
708
709 ########################################################################
710 # GUI theming
711 ########################################################################
712
713 var nextStyle = func {
714     var curr = getprop("/sim/gui/current-style");
715     var styles = props.globals.getNode("/sim/gui").getChildren("style");
716     forindex (var i; styles)
717         if (styles[i].getIndex() == curr)
718             break;
719     if ((i += 1) >= size(styles))
720         i = 0;
721     setprop("/sim/gui/current-style", styles[i].getIndex());
722     fgcommand("gui-redraw");
723 }
724
725
726 ########################################################################
727 # Dialog Boxes
728 ########################################################################
729
730 var dialog = {};
731
732 var setWeight = func(wgt, opt) {
733     var lbs = opt.getNode("lbs", 1).getValue();
734     wgt.getNode("weight-lb", 1).setValue(lbs);
735
736     # Weights can have "tank" indices which set the capacity of the
737     # corresponding tank.  This code should probably be moved to
738     # something like fuel.setTankCap(tank, gals)...
739     if(wgt.getNode("tank",0) == nil) { return 0; }
740     var ti = wgt.getNode("tank").getValue();
741     var gn = opt.getNode("gals");
742     var gals = gn == nil ? 0 : gn.getValue();
743     var tn = props.globals.getNode("consumables/fuel/tank["~ti~"]", 1);
744     var ppg = tn.getNode("density-ppg", 1).getValue();
745     var lbs = gals * ppg;
746     var curr = tn.getNode("level-gal_us", 1).getValue();
747     curr = curr > gals ? gals : curr;
748     tn.getNode("capacity-gal_us", 1).setValue(gals);
749     tn.getNode("level-gal_us", 1).setValue(curr);
750     tn.getNode("level-lbs", 1).setValue(curr * ppg);
751     return 1;
752 }
753
754 # Checks the /sim/weight[n]/{selected|opt} values and sets the
755 # appropriate weights therefrom.
756 var setWeightOpts = func {
757     var tankchange = 0;
758     foreach(var w; props.globals.getNode("sim").getChildren("weight")) {
759         var selected = w.getNode("selected");
760         if(selected != nil) {
761             foreach(var opt; w.getChildren("opt")) {
762                 if(opt.getNode("name", 1).getValue() == selected.getValue()) {
763                     if(setWeight(w, opt)) { tankchange = 1; }
764                     break;
765                 }
766             }
767         }
768     }
769     return tankchange;
770 }
771 # Run it at startup and on reset to make sure the tank settings are correct
772 _setlistener("/sim/signals/fdm-initialized", func { settimer(setWeightOpts, 0) });
773 _setlistener("/sim/signals/reinit", func(n) { props._getValue(n, []) or setWeightOpts() });
774
775
776 # Called from the F&W dialog when the user selects a weight option
777 var weightChangeHandler = func {
778     var tankchanged = setWeightOpts();
779
780     # This is unfortunate.  Changing tanks means that the list of
781     # tanks selected and their slider bounds must change, but our GUI
782     # isn't dynamic in that way.  The only way to get the changes on
783     # screen is to pop it down and recreate it.
784     if(tankchanged) {
785         var p = props.Node.new({"dialog-name": "WeightAndFuel"});
786         fgcommand("dialog-close", p);
787         showWeightDialog();
788     }
789 }
790
791
792
793 ##
794 # Dynamically generates a weight & fuel configuration dialog specific to
795 # the aircraft.
796 #
797 var showWeightDialog = func {
798     var name = "WeightAndFuel";
799     var title = "Weight and Fuel Settings";
800
801     #
802     # General Dialog Structure
803     #
804     dialog[name] = Widget.new();
805     dialog[name].set("name", name);
806     dialog[name].set("layout", "vbox");
807
808     var header = dialog[name].addChild("group");
809     header.set("layout", "hbox");
810     header.addChild("empty").set("stretch", "1");
811     header.addChild("text").set("label", title);
812     header.addChild("empty").set("stretch", "1");
813     var w = header.addChild("button");
814     w.set("pref-width", 16);
815     w.set("pref-height", 16);
816     w.set("legend", "");
817     w.set("default", 0);
818     w.setBinding("dialog-close");
819
820     dialog[name].addChild("hrule");
821
822     if (fdm != "yasim" and fdm != "jsb") {
823         var msg = dialog[name].addChild("text");
824         msg.set("label", "Not supported for this aircraft");
825         var cancel = dialog[name].addChild("button");
826         cancel.set("key", "Esc");
827         cancel.set("legend", "Cancel");
828         cancel.setBinding("dialog-close");
829         fgcommand("dialog-new", dialog[name].prop());
830         showDialog(name);
831         return;
832     }
833
834     # FDM dependent settings
835     if(fdm == "yasim") {
836         var fdmdata = {
837             grosswgt : "/yasim/gross-weight-lbs",
838             payload  : "/sim",
839             cg       : nil,
840         };
841     } elsif(fdm == "jsb") {
842         var fdmdata = {
843             grosswgt : "/fdm/jsbsim/inertia/weight-lbs",
844             payload  : "/payload",
845             cg       : "/fdm/jsbsim/inertia/cg-x-in",
846         };
847     }
848
849     var contentArea = dialog[name].addChild("group");
850     contentArea.set("layout", "hbox");
851     contentArea.set("default-padding", 10);
852
853     dialog[name].addChild("empty");
854
855     var limits = dialog[name].addChild("group");
856     limits.set("layout", "table");
857     limits.set("halign", "center");
858     var row = 0;
859
860     var massLimits = props.globals.getNode("/limits/mass-and-balance");
861
862     var tablerow = func(name, node, format ) {
863
864         var n = isa( node, props.Node ) ? node : massLimits.getNode( node );
865         if( n == nil ) return;
866
867         var label = limits.addChild("text");
868         label.set("row", row);
869         label.set("col", 0);
870         label.set("halign", "right");
871         label.set("label", name ~ ":");
872
873         var val = limits.addChild("text");
874         val.set("row", row);
875         val.set("col", 1);
876         val.set("halign", "left");
877         val.set("label", "0123457890123456789");
878         val.set("format", format);
879         val.set("property", n.getPath());
880         val.set("live", 1);
881           
882         row += 1;
883     }
884
885     var grossWgt = props.globals.getNode(fdmdata.grosswgt);
886     if(grossWgt != nil) {
887         tablerow("Gross Weight", grossWgt, "%.0f lb");
888     }
889
890     if(massLimits != nil ) {
891         tablerow("Max. Ramp Weight", "maximum-ramp-mass-lbs", "%.0f lb" );
892         tablerow("Max. Takeoff  Weight", "maximum-takeoff-mass-lbs", "%.0f lb" );
893         tablerow("Max. Landing  Weight", "maximum-landing-mass-lbs", "%.0f lb" );
894         tablerow("Max. Arrested Landing  Weight", "maximum-arrested-landing-mass-lbs", "%.0f lb" );
895         tablerow("Max. Zero Fuel Weight", "maximum-zero-fuel-mass-lbs", "%.0f lb" );
896     }
897
898     if( fdmdata.cg != nil ) { 
899         var n = props.globals.getNode("/limits/mass-and-balance/cg/dimension");
900         tablerow("Center of Gravity", props.globals.getNode(fdmdata.cg), "%.1f " ~ (n == nil ? "in" : n.getValue()));
901     }
902
903     dialog[name].addChild("hrule");
904
905     var buttonBar = dialog[name].addChild("group");
906     buttonBar.set("layout", "hbox");
907     buttonBar.set("default-padding", 10);
908
909     var close = buttonBar.addChild("button");
910     close.set("legend", "Close");
911     close.set("default", "true");
912     close.set("key", "Enter");
913     close.setBinding("dialog-close");
914
915     # Temporary helper function
916     var tcell = func(parent, type, row, col) {
917         var cell = parent.addChild(type);
918         cell.set("row", row);
919         cell.set("col", col);
920         return cell;
921     }
922
923     #
924     # Fill in the content area
925     #
926     var fuelArea = contentArea.addChild("group");
927     fuelArea.set("layout", "vbox");
928     fuelArea.addChild("text").set("label", "Fuel Tanks");
929
930     var fuelTable = fuelArea.addChild("group");
931     fuelTable.set("layout", "table");
932
933     fuelArea.addChild("empty").set("stretch", 1);
934
935     tcell(fuelTable, "text", 0, 0).set("label", "Tank");
936     tcell(fuelTable, "text", 0, 3).set("label", "Pounds");
937     tcell(fuelTable, "text", 0, 4).set("label", "Gallons");
938     tcell(fuelTable, "text", 0, 5).set("label", "Fraction");
939
940     var tanks = props.globals.getNode("/consumables/fuel").getChildren("tank");
941     for(var i=0; i<size(tanks); i+=1) {
942         var t = tanks[i];
943
944         var tname = i ~ "";
945         var tnode = t.getNode("name");
946         if(tnode != nil) { tname = tnode.getValue(); }
947
948         var tankprop = "/consumables/fuel/tank["~i~"]";
949
950         var cap = t.getNode("capacity-gal_us", 0);
951
952         # Hack, to ignore the "ghost" tanks created by the C++ code.
953         if(cap == nil ) { continue; }
954         cap = cap.getValue();
955
956         # Ignore tanks of capacity 0
957         if (cap == 0) { continue; }
958
959         var title = tcell(fuelTable, "text", i+1, 0);
960         title.set("label", tname);
961         title.set("halign", "right");
962
963         var selected = props.globals.initNode(tankprop ~ "/selected", 1, "BOOL");
964         if (selected.getAttribute("writable")) {
965             var sel = tcell(fuelTable, "checkbox", i+1, 1);
966             sel.set("property", tankprop ~ "/selected");
967             sel.set("live", 1);
968             sel.setBinding("dialog-apply");
969         }
970
971         var slider = tcell(fuelTable, "slider", i+1, 2);
972         slider.set("property", tankprop ~ "/level-gal_us");
973         slider.set("live", 1);
974         slider.set("min", 0);
975         slider.set("max", cap);
976         slider.setBinding("dialog-apply");
977
978         var lbs = tcell(fuelTable, "text", i+1, 3);
979         lbs.set("property", tankprop ~ "/level-lbs");
980         lbs.set("label", "0123456");
981         lbs.set("format", cap < 1 ? "%.3f" : cap < 10 ? "%.2f" : "%.1f" );
982         lbs.set("halign", "right");
983         lbs.set("live", 1);
984
985         var gals = tcell(fuelTable, "text", i+1, 4);
986         gals.set("property", tankprop ~ "/level-gal_us");
987         gals.set("label", "0123456");
988         gals.set("format", cap < 1 ? "%.3f" : cap < 10 ? "%.2f" : "%.1f" );
989         gals.set("halign", "right");
990         gals.set("live", 1);
991
992         var per = tcell(fuelTable, "text", i+1, 5);
993         per.set("property", tankprop ~ "/level-norm");
994         per.set("label", "0123456");
995         per.set("format", "%.2f");
996         per.set("halign", "right");
997         per.set("live", 1);
998     }
999
1000     varbar = tcell(fuelTable, "hrule", size(tanks)+1, 0);
1001     varbar.set("colspan", 6);
1002
1003     var total_label = tcell(fuelTable, "text", size(tanks)+2, 2);
1004     total_label.set("label", "Total:");
1005     total_label.set("halign", "right");
1006
1007     var lbs = tcell(fuelTable, "text", size(tanks)+2, 3);
1008     lbs.set("property", "/consumables/fuel/total-fuel-lbs");
1009     lbs.set("label", "0123456");
1010     lbs.set("format", "%.1f" );
1011     lbs.set("halign", "right");
1012     lbs.set("live", 1);
1013
1014     var gals = tcell(fuelTable, "text",size(tanks) +2, 4);
1015     gals.set("property", "/consumables/fuel/total-fuel-gal_us");
1016     gals.set("label", "0123456");
1017     gals.set("format", "%.1f" );
1018     gals.set("halign", "right");
1019     gals.set("live", 1);
1020
1021     var per = tcell(fuelTable, "text", size(tanks)+2, 5);
1022     per.set("property", "/consumables/fuel/total-fuel-norm");
1023     per.set("label", "0123456");
1024     per.set("format", "%.2f");
1025     per.set("halign", "right");
1026     per.set("live", 1);
1027
1028     var weightArea = contentArea.addChild("group");
1029     weightArea.set("layout", "vbox");
1030     weightArea.addChild("text").set("label", "Payload");
1031
1032     var weightTable = weightArea.addChild("group");
1033     weightTable.set("layout", "table");
1034
1035     weightArea.addChild("empty").set("stretch", 1);
1036
1037     tcell(weightTable, "text", 0, 0).set("label", "Location");
1038     tcell(weightTable, "text", 0, 2).set("label", "Pounds");
1039
1040     var payload_base = props.globals.getNode(fdmdata.payload);
1041     if (payload_base != nil)
1042         var wgts = payload_base.getChildren("weight");
1043     else
1044         var wgts = [];
1045     for(var i=0; i<size(wgts); i+=1) {
1046         var w = wgts[i];
1047         var wname = w.getNode("name", 1).getValue();
1048         var wprop = fdmdata.payload ~ "/weight[" ~ i ~ "]";
1049
1050         var title = tcell(weightTable, "text", i+1, 0);
1051         title.set("label", wname);
1052         title.set("halign", "right");
1053
1054         if(w.getNode("opt") != nil) {
1055             var combo = tcell(weightTable, "combo", i+1, 1);
1056             combo.set("property", wprop ~ "/selected");
1057             combo.set("pref-width", 300);
1058
1059             # Simple code we'd like to use:
1060             #foreach(opt; w.getChildren("opt")) {
1061             #    var ent = combo.addChild("value");
1062             #    ent.prop().setValue(opt.getNode("name", 1).getValue());
1063             #}
1064
1065             # More complicated workaround to move the "current" item
1066             # into the first slot, because dialog.cxx doesn't set the
1067             # selected item in the combo box.
1068             var opts = [];
1069             var curr = w.getNode("selected");
1070             curr = curr == nil ? "" : curr.getValue();
1071             foreach(opt; w.getChildren("opt")) {
1072                 append(opts, opt.getNode("name", 1).getValue());
1073             }
1074             forindex(oi; opts) {
1075                 if(opts[oi] == curr) {
1076                     var tmp = opts[0];
1077                     opts[0] = opts[oi];
1078                     opts[oi] = tmp;
1079                     break;
1080                 }
1081             }
1082             foreach(opt; opts) {
1083                 combo.addChild("value").prop().setValue(opt);
1084             }
1085
1086             combo.setBinding("dialog-apply");
1087             combo.setBinding("nasal", "gui.weightChangeHandler()");
1088         } else {
1089             var slider = tcell(weightTable, "slider", i+1, 1);
1090             slider.set("property", wprop ~ "/weight-lb");
1091             var min = w.getNode("min-lb", 1).getValue();
1092             var max = w.getNode("max-lb", 1).getValue();
1093             slider.set("min", min != nil ? min : 0);
1094             slider.set("max", max != nil ? max : 100);
1095             slider.set("live", 1);
1096             slider.setBinding("dialog-apply");
1097         }
1098
1099         var lbs = tcell(weightTable, "text", i+1, 2);
1100         lbs.set("property", wprop ~ "/weight-lb");
1101         lbs.set("label", "0123456");
1102         lbs.set("format", "%.0f");
1103         lbs.set("live", 1);
1104     }
1105
1106     # All done: pop it up
1107     fgcommand("dialog-new", dialog[name].prop());
1108     showDialog(name);
1109 }
1110
1111
1112
1113
1114 ##
1115 # Dynamically generates a dialog from a help node.
1116 #
1117 # gui.showHelpDialog([<path> [, toggle]])
1118 #
1119 # path   ... path to help node
1120 # toggle ... decides if an already open dialog should be closed
1121 #            (useful when calling the dialog from a key binding; default: 0)
1122 #
1123 # help node
1124 # =========
1125 # each of <title>, <key>, <line>, <text> is optional; uses
1126 # "/sim/description" or "/sim/aircraft" if <title> is omitted;
1127 # only the first <text> is displayed
1128 #
1129 #
1130 # <help>
1131 #     <title>dialog title<title>
1132 #     <key>
1133 #         <name>g/G</name>
1134 #         <desc>gear up/down</desc>
1135 #     </key>
1136 #
1137 #     <line>one line</line>
1138 #     <line>another line</line>
1139 #
1140 #     <text>text in
1141 #           scrollable widget
1142 #     </text>
1143 # </help>
1144 #
1145 var showHelpDialog = func {
1146     var node = props.globals.getNode(arg[0]);
1147     if (arg[0] == "/sim/help" and size(node.getChildren()) < 4) {
1148         node = node.getChild("common");
1149     }
1150
1151     var name = node.getNode("title", 1).getValue();
1152     if (name == nil) {
1153         name = getprop("/sim/description");
1154         if (name == nil) {
1155             name = getprop("/sim/aircraft");
1156         }
1157     }
1158     var toggle = size(arg) > 1 and arg[1] != nil and arg[1] > 0;
1159     if (toggle and contains(dialog, name)) {
1160         fgcommand("dialog-close", props.Node.new({ "dialog-name": name }));
1161         delete(dialog, name);
1162         return;
1163     }
1164
1165     dialog[name] = Widget.new();
1166     dialog[name].set("layout", "vbox");
1167     dialog[name].set("default-padding", 0);
1168     dialog[name].set("name", name);
1169
1170     # title bar
1171     var titlebar = dialog[name].addChild("group");
1172     titlebar.set("layout", "hbox");
1173     titlebar.addChild("empty").set("stretch", 1);
1174     titlebar.addChild("text").set("label", name);
1175     titlebar.addChild("empty").set("stretch", 1);
1176
1177     var w = titlebar.addChild("button");
1178     w.set("pref-width", 16);
1179     w.set("pref-height", 16);
1180     w.set("legend", "");
1181     w.set("default", 1);
1182     w.set("key", "esc");
1183     w.setBinding("nasal", "delete(gui.dialog, \"" ~ name ~ "\")");
1184     w.setBinding("dialog-close");
1185
1186     dialog[name].addChild("hrule");
1187
1188     # key list
1189     var keylist = dialog[name].addChild("group");
1190     keylist.set("layout", "table");
1191     keylist.set("default-padding", 2);
1192     var keydefs = node.getChildren("key");
1193     var n = size(keydefs);
1194     var row = var col = 0;
1195     foreach (var key; keydefs) {
1196         if (n >= 60 and row >= n / 3 or n >= 16 and row >= n / 2) {
1197             col += 1;
1198             row = 0;
1199         }
1200
1201         var w = keylist.addChild("text");
1202         w.set("row", row);
1203         w.set("col", 2 * col);
1204         w.set("halign", "right");
1205         w.set("label", " " ~ key.getNode("name").getValue());
1206
1207         w = keylist.addChild("text");
1208         w.set("row", row);
1209         w.set("col", 2 * col + 1);
1210         w.set("halign", "left");
1211         w.set("label", "... " ~ key.getNode("desc").getValue() ~ "  ");
1212         row += 1;
1213     }
1214
1215     # separate lines
1216     var lines = node.getChildren("line");
1217     if (size(lines)) {
1218         if (size(keydefs)) {
1219             dialog[name].addChild("empty").set("pref-height", 4);
1220             dialog[name].addChild("hrule");
1221             dialog[name].addChild("empty").set("pref-height", 4);
1222         }
1223
1224         var g = dialog[name].addChild("group");
1225         g.set("layout", "vbox");
1226         g.set("default-padding", 1);
1227         foreach (var lin; lines) {
1228             foreach (var l; split("\n", lin.getValue())) {
1229                 var w = g.addChild("text");
1230                 w.set("halign", "left");
1231                 w.set("label", " " ~ l ~ " ");
1232             }
1233         }
1234     }
1235
1236     # scrollable text area
1237     if (node.getNode("text") != nil) {
1238         dialog[name].set("resizable", 1);
1239         dialog[name].addChild("empty").set("pref-height", 10);
1240
1241         var width = [640, 800, 1152][col];
1242         var height = screenHProp.getValue() - (100 + (size(keydefs) / (col + 1) + size(lines)) * 28);
1243         if (height < 200) {
1244             height = 200;
1245         }
1246
1247         var w = dialog[name].addChild("textbox");
1248         w.set("padding", 4);
1249         w.set("halign", "fill");
1250         w.set("valign", "fill");
1251         w.set("stretch", "true");
1252         w.set("slider", 20);
1253         w.set("pref-width", width);
1254         w.set("pref-height", height);
1255         w.set("editable", 0);
1256         w.set("property", node.getPath() ~ "/text");
1257     } else {
1258         dialog[name].addChild("empty").set("pref-height", 8);
1259     }
1260     fgcommand("dialog-new", dialog[name].prop());
1261     showDialog(name);
1262 }
1263
1264
1265 var debug_keys = {
1266     title: "Development Keys",
1267     key: [
1268        #{ name: "Ctrl-U",    desc: "add 1000 ft of emergency altitude" },
1269         { name: "Shift-F3",  desc: "load panel" },
1270         { name: "/",         desc: "open property browser" },
1271     ],
1272 };
1273
1274 var basic_keys = {
1275     title: "Basic Keys",
1276     key: [
1277         { name: "?",         desc: "show/hide aircraft help dialog" },
1278        #{ name: "Tab",       desc: "show/hide aircraft config dialog" },
1279         { name: "Esc",       desc: "quit FlightGear" },
1280         { name: "Shift-Esc", desc: "reset FlightGear" },
1281         { name: "a/A",       desc: "increase/decrease speed-up" },
1282         { name: "c",         desc: "toggle 3D/2D cockpit" },
1283         { name: "Ctrl-C",    desc: "toggle clickable panel hotspots" },
1284         { name: "p",         desc: "pause/continue sim" },
1285         { name: "Ctrl-R",    desc: "activate instant replay system" },
1286         { name: "t/T",       desc: "increase/decrease warp delta" },
1287         { name: "v/V",       desc: "cycle views (forward/backward)" },
1288         { name: "Ctrl-V",    desc: "select cockpit view" },
1289         { name: "w/W",       desc: "increase/decrease warp" },
1290         { name: "x/X",       desc: "zoom in/out" },
1291         { name: "Ctrl-X",    desc: "reset zoom to default" },
1292         { name: "z/Z",       desc: "increase/decrease visibility" },
1293         { name: "Ctrl-Z",    desc: "reset visibility to default" },
1294         { name: "'",         desc: "display ATC setting dialog" },
1295         { name: "+",         desc: "let ATC/instructor repeat last message" },
1296         { name: "-",         desc: "open chat dialog" },
1297         { name: "_",         desc: "compose chat message" },
1298         { name: "F3",        desc: "capture screen" },
1299         { name: "F10",       desc: "toggle menubar" },
1300        #{ name: "Shift-F1",  desc: "load flight" },
1301        #{ name: "Shift-F2",  desc: "save flight" },
1302         { name: "Shift-F10", desc: "cycle through GUI styles" },
1303     ],
1304 };
1305
1306 var common_aircraft_keys = {
1307     title: "Common Aircraft Keys",
1308     key: [
1309         { name: "Enter",     desc: "move rudder right" },
1310         { name: "0/Insert",  desc: "move rudder left" },
1311         { name: "1/End",     desc: "elevator trim up" },
1312         { name: "2/Down",    desc: "elevator up or increase AP altitude" },
1313         { name: "3/PgDn",    desc: "decr. throttle or AP autothrottle" },
1314         { name: "4/Left",    desc: "move aileron left or adj. AP hdg." },
1315         { name: "5/KP5",     desc: "center aileron, elev., and rudder" },
1316         { name: "6/Right",   desc: "move aileron right or adj. AP hdg." },
1317         { name: "7/Home",    desc: "elevator trim down" },
1318         { name: "8/Up",      desc: "elevator down or decrease AP altitude" },
1319         { name: "9/PgUp",    desc: "incr. throttle or AP autothrottle" },
1320         { name: "Space",     desc: "PTT - Push To Talk (via VoIP)" },
1321         { name: "!/@/#/$",   desc: "select engine 1/2/3/4" },
1322         { name: "b",         desc: "apply all brakes" },
1323         { name: "B",         desc: "toggle parking brake" },
1324        #{ name: "Ctrl-B",    desc: "toggle speed brake" },
1325         { name: "g/G",       desc: "gear up/down" },
1326         { name: "h",         desc: "cycle HUD (head up display)" },
1327         { name: "H",         desc: "cycle HUD brightness" },
1328        #{ name: "i/Shift-i", desc: "normal/alternative HUD" },
1329        #{ name: "j",         desc: "decrease spoilers" },
1330        #{ name: "k",         desc: "increase spoilers" },
1331         { name: "l",         desc: "toggle tail-wheel lock" },
1332         { name: "m/M",       desc: "mixture richer/leaner" },
1333         { name: "n/N",       desc: "propeller finer/coarser" },
1334         { name: "P",         desc: "toggle 2D panel" },
1335         { name: "S",         desc: "swap panels" },
1336         { name: "s",         desc: "fire starter on selected eng." },
1337         { name: ", .",       desc: "left/right brake (comma, period)" },
1338         { name: "~",         desc: "select all engines (tilde)" },
1339         { name: "[ ]",       desc: "flaps up/down" },
1340         { name: "{ }",       desc: "decr/incr magneto on sel. eng." },
1341         { name: "Ctrl-A",    desc: "AP: toggle altitude lock" },
1342         { name: "Ctrl-G",    desc: "AP: toggle glide slope lock" },
1343         { name: "Ctrl-H",    desc: "AP: toggle heading lock" },
1344         { name: "Ctrl-N",    desc: "AP: toggle NAV1 lock" },
1345         { name: "Ctrl-P",    desc: "AP: toggle pitch hold" },
1346         { name: "Ctrl-S",    desc: "AP: toggle auto-throttle" },
1347         { name: "Ctrl-T",    desc: "AP: toggle terrain lock" },
1348         { name: "Ctrl-W",    desc: "AP: toggle wing leveler" },
1349         { name: "F6",        desc: "AP: toggle heading mode" },
1350         { name: "F11",       desc: "open autopilot dialog" },
1351         { name: "F12",       desc: "open radio settings dialog" },
1352         { name: "Shift-F5",  desc: "scroll 2D panel down" },
1353         { name: "Shift-F6",  desc: "scroll 2D panel up" },
1354         { name: "Shift-F7",  desc: "scroll 2D panel left" },
1355         { name: "Shift-F8",  desc: "scroll 2D panel right" },
1356     ],
1357 };
1358
1359 _setlistener("/sim/signals/screenshot", func {
1360      var path = getprop("/sim/paths/screenshot-last");
1361      var button = { button: { legend: "Ok", default: 1, binding: { command: "dialog-close" }}};
1362      var success= getprop("/sim/signals/screenshot");
1363      if (success) {
1364          popupTip("Screenshot written to '" ~ path ~ "'", 3);
1365      } else {
1366          popupTip("Error writing screenshot '" ~ path ~ "'", 600, button);
1367      }
1368 });
1369
1370 var terrasync_stalled = 0;
1371 _setlistener("/sim/terrasync/stalled", func {
1372      var stalled = getprop("/sim/terrasync/stalled");
1373      if (stalled and !terrasync_stalled)
1374      {
1375          var button = { button: { legend: "Ok", default: 1, binding: { command: "dialog-close" }}};
1376          popupTip("Scenery download stalled. Too many errors reported. See log output.", 600, button);
1377      }
1378      terrasync_stalled = stalled;
1379 });
1380
1381 var do_welcome = 1;
1382 _setlistener("/sim/signals/fdm-initialized", func {
1383     var haveTutorials = size(props.globals.getNode("/sim/tutorials", 1).getChildren("tutorial"));
1384     gui.menuEnable("tutorial-start", haveTutorials);
1385     if (do_welcome and haveTutorials)
1386         settimer(func { setprop("/sim/messages/copilot", "Welcome aboard! Need help? Use 'Help -> Tutorials'.");}, 5.0);
1387     do_welcome = 0;
1388 });
1389
1390 # load ATC chatter module on demand
1391 setprop("/nasal/atc-chatter/enabled", getprop("/sim/sound/chatter/enabled"));
1392 _setlistener("/sim/sound/chatter/enabled", func {
1393     setprop("/nasal/atc-chatter/enabled", getprop("/sim/sound/chatter/enabled"));
1394 });
1395
1396 ##
1397 # overwrite custom shader settings when quality-level is set on startup
1398 var qualityLevel = getprop("/sim/rendering/shaders/quality-level");
1399 var rembrandtOn = getprop("/sim/rendering/rembrandt/enabled");
1400 if (qualityLevel == -1) {
1401     setprop("/sim/rendering/shaders/custom-settings",1);
1402 }
1403 elsif (qualityLevel != nil) {
1404     setprop("/sim/rendering/shaders/custom-settings",0);
1405     setprop("/sim/rendering/shaders/quality-level-internal",qualityLevel);
1406     if (qualityLevel == 0) {
1407         setprop("/sim/rendering/shaders/skydome",0);
1408     }
1409 }
1410 # overwrite custom shader settings when quality-level is set through the slider
1411 # in the Rendering Options dialog
1412 var update_shader_settings = func() {
1413     if (!getprop("/sim/rendering/shaders/custom-settings")){
1414         var qualityLvl = getprop("/sim/rendering/shaders/quality-level-internal");
1415                 setprop("/sim/rendering/shaders/quality-level", qualityLvl);
1416         setprop("/sim/rendering/shaders/landmass",qualityLvl);
1417         setprop("/sim/rendering/shaders/urban",qualityLvl);
1418         setprop("/sim/rendering/shaders/water",qualityLvl);
1419         if (qualityLvl >= 3.0){
1420             qualityLvl = 3.0;
1421         }
1422         setprop("/sim/rendering/shaders/model",qualityLvl);
1423         if (qualityLvl >= 1.0){
1424             qualityLvl = 1.0;
1425         }
1426         setprop("/sim/rendering/shaders/contrails",qualityLvl);
1427         setprop("/sim/rendering/shaders/crop",qualityLvl);
1428         setprop("/sim/rendering/shaders/generic",qualityLvl);
1429         setprop("/sim/rendering/shaders/transition",qualityLvl);
1430     } else {
1431                 setprop("/sim/rendering/shaders/quality-level",-1);
1432         }
1433
1434     if (rembrandtOn) {
1435                 setprop("/sim/rendering/shaders/skydome",0);
1436         }
1437 };
1438 _setlistener("/sim/rendering/shaders/custom-settings", func { update_shader_settings() } );
1439 _setlistener("/sim/rendering/shaders/quality-level-internal",   func { update_shader_settings() } );
1440 update_shader_settings();