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,
9 var popupTip = func(label, delay = nil, override = nil)
11 fgcommand("show-message", props.Node.new({ "label": label, "delay":delay }));
16 var showDialog = func(name) {
17 fgcommand("dialog-show", props.Node.new({ "dialog-name" : name }));
21 # Enable/disable named menu entry
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);
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);
41 # Set the binding for a menu item to a Nasal script,
42 # typically a dialog open() command.
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");
59 # Set mouse cursor coordinates and shape (number or name), and return
60 # current shape (number).
62 # Example: var cursor = gui.setCursor();
63 # gui.setCursor(nil, nil, "wait");
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);
70 if (num(cursor) == nil)
71 cursor = cursor_types[cursor];
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);
77 fgcommand("set-cursor", args);
78 return args.getNode("cursor").getValue();
82 # Supported mouse cursor types.
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,
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.
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 )
100 var f = findElementByName(child, name);
101 if( f != nil ) return f;
107 ########################################################################
109 ########################################################################
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...
117 var fdm = getprop("/sim/flight-model");
118 var screenHProp = nil;
120 var autopilotDisableProps = [
121 "/autopilot/hide-menu",
122 "/autopilot/KAP140/locks",
123 "/autopilot/CENTURYIIB/locks",
124 "/autopilot/CENTURYIII/locks"
127 _setlistener("/sim/signals/nasal-dir-initialized", func {
128 screenHProp = props.globals.getNode("/sim/startup/ysize");
129 tipArg = props.Node.new({ "dialog-name" : "PopTip" });
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);
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 )
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"));
152 # frame-per-second display
153 var fps = props.globals.getNode("/sim/rendering/fps-display", 1);
154 setlistener(fps, fpsDisplay, 1);
155 setlistener("/sim/startup/xsize", func {
156 if (fps.getValue()) {
162 # frame-latency display
163 var latency = props.globals.getNode("/sim/rendering/frame-latency-display", 1);
164 setlistener(latency, latencyDisplay, 1);
165 setlistener("/sim/startup/xsize", func {
166 if (latency.getValue()) {
172 # only enable precipitation if gui *and* aircraft want it
173 var p = "/sim/rendering/precipitation-";
174 var precip_gui = getprop(p ~ "gui-enable");
175 var precip_ac = getprop(p ~ "aircraft-enable");
176 props.globals.getNode(p ~ "enable").setAttribute("userarchive", 0); # TODO remove later
177 var set_precip = func setprop(p ~ "enable", precip_gui and precip_ac);
178 setlistener(p ~ "gui-enable", func(n) set_precip(precip_gui = n.getValue()),1);
179 setlistener(p ~ "aircraft-enable", func(n) set_precip(precip_ac = n.getValue()),1);
181 # the autovisibility feature of the menubar
182 # automatically show the menubar if the mouse is at the upper edge of the window
183 # the menubar is hidden by a binding to a LMB click in mode 0 in mice.xml
184 var menubarAutoVisibilityListener = nil;
185 var menubarAutoVisibilityEdge = props.globals.initNode( "/sim/menubar/autovisibility/edge-size", 5, "INT" );
186 var menubarVisibility = props.globals.initNode( "/sim/menubar/visibility", 0, "BOOL" );
187 var currentMenubarVisibility = menubarVisibility.getValue();
188 var mouseMode = props.globals.initNode( "/devices/status/mice/mouse/mode", 0, "INT" );
190 setlistener( "/sim/menubar/autovisibility/enabled", func(n) {
191 if( n.getValue() and menubarAutoVisibilityListener == nil ) {
192 currentMenubarVisibility = menubarVisibility.getValue();
193 menubarVisibility.setBoolValue( 0 );
194 menubarAutoVisibilityListener = setlistener( "/devices/status/mice/mouse/y", func(n) {
195 if( n.getValue() == nil ) return;
196 if( mouseMode.getValue() != 0 ) return;
198 if( n.getValue() <= menubarAutoVisibilityEdge.getValue() )
199 menubarVisibility.setBoolValue( 1 );
204 # don't listen to the mouse position if this feature is enabled
205 if( n.getValue() == 0 and menubarAutoVisibilityListener != nil ) {
206 removelistener( menubarAutoVisibilityListener );
207 menubarAutoVisibilityListener = nil;
208 menubarVisibility.setBoolValue(currentMenubarVisibility);
216 # Show/hide the fps display dialog.
218 var fpsDisplay = func(n) {
219 var w = isa(n, props.Node) ? n.getValue() : n;
220 fgcommand(w ? "dialog-show" : "dialog-close", props.Node.new({"dialog-name": "fps"}));
222 var latencyDisplay = func(n) {
223 var w = isa(n, props.Node) ? n.getValue() : n;
224 fgcommand(w ? "dialog-show" : "dialog-close", props.Node.new({"dialog-name": "frame-latency"}));
228 # How many seconds do we show the tip?
233 # Pop down the tip dialog, if it is visible.
235 var popdown = func { fgcommand("dialog-close", tipArg); }
237 # Marker for the "current" timer. This value gets stored in the
238 # closure of the timer function, and is used to check that there
239 # hasn't been a more recent timer set that should override.
242 ########################################################################
243 # Widgets & Layout Management
244 ########################################################################
247 # A "widget" class that wraps a property node. It provides useful
248 # helper methods that are difficult or tedious with the raw property
249 # API. Note especially the slightly tricky addChild() method.
252 set : func { me.node.getNode(arg[0], 1).setValue(arg[1]); },
253 prop : func { return me.node; },
254 new : func { return { parents : [Widget], node : props.Node.new() } },
257 var idx = size(me.node.getChildren(type));
258 var name = type ~ "[" ~ idx ~ "]";
259 var newnode = me.node.getNode(name, 1);
260 return { parents : [Widget], node : newnode };
262 setColor : func(r, g, b, a = 1) {
263 me.node.setValues({ color : { red:r, green:g, blue:b, alpha:a } });
265 setFont : func(n, s = 13, t = 0) {
266 me.node.setValues({ font : { name:n, "size":s, slant:t } });
268 setBinding : func(cmd, carg = nil) {
269 var idx = size(me.node.getChildren("binding"));
270 var node = me.node.getChild("binding", idx, 1);
271 node.getNode("command", 1).setValue(cmd);
272 if (cmd == "nasal") {
273 node.getNode("script", 1).setValue(carg);
274 } elsif (carg != nil and (cmd == "dialog-apply" or cmd == "dialog-update")) {
275 node.getNode("object-name", 1).setValue(carg);
282 # Dialog class. Maintains one XML dialog.
285 # (B) Dialog.new(<dialog-name>); ... use dialog from $FG_ROOT/gui/dialogs/
287 # (A) Dialog.new(<prop>, <path> [, <dialog-name>]);
288 # ... load aircraft specific dialog from
289 # <path> under property <prop> and under
290 # name <dialog-name>; if no name is given,
291 # then it's taken from the XML dialog
293 # prop ... target node (name must be "dialog")
294 # path ... file path relative to $FG_ROOT
295 # dialog-name ... dialog <name> of dialog in $FG_ROOT/gui/dialogs/
299 # var dlg = gui.Dialog.new("/sim/gui/dialogs/foo-config/dialog",
300 # "Aircraft/foo/foo_config.xml");
304 # var livery_dialog = gui.Dialog.new("livery-select");
305 # livery_dialog.toggle();
308 new: func(prop, path = nil, name = nil) {
309 var m = { parents: [Dialog] };
312 if (path == nil) { # global dialog in $FG_ROOT/gui/dialogs/
314 m.prop = props.Node.new({ "dialog-name" : prop });
315 } else { # aircraft dialog with given path
318 m.prop = isa(prop, props.Node) ? prop : props.globals.getNode(prop, 1);
319 if (m.prop.getName() != "dialog")
320 die("Dialog class: node name must end with '/dialog'");
322 m.listener = setlistener("/sim/signals/reinit-gui", func m.load(), 1);
324 return Dialog.instance[m.name] = m;
328 if (me.listener != nil)
329 removelistener(me.listener);
331 # doesn't need to be called explicitly, but can be used to force a reload
333 var state = me.state;
337 me.prop.removeChildren();
338 io.read_properties(me.path, me.prop);
340 var n = me.prop.getNode("name");
342 die("Dialog class: XML dialog must have <name>");
345 me.name = n.getValue();
349 me.prop.getNode("dialog-name", 1).setValue(me.name);
350 fgcommand("dialog-new", me.prop);
354 # allows access to dialog-embedded Nasal variables/functions
356 var ns = "__dlg:" ~ me.name;
357 me.state and contains(globals, ns) ? globals[ns] : nil;
360 fgcommand("dialog-show", me.prop);
364 fgcommand("dialog-close", me.prop);
368 me.state ? me.close() : me.open();
378 # Overlay selector. Displays a list of overlay XML files and copies the
379 # chosen one to the property tree. The class allows to select liveries,
380 # insignia, decals, variants, etc. Usually the overlay properties are
381 # fed to "select" and "material" animations.
384 # OverlaySelector.new(<title>, <dir>, <nameprop> [, <sortprop> [, <mpprop> [, <callback>]]]);
386 # title ... dialog title
387 # dir ... directory where to find the XML overlay files,
388 # relative to FG_ROOT
389 # nameprop ... property in an overlay file that contains the name
390 # The result is written to this place in the
392 # sortprop ... property in an overlay file that should be used
393 # as sorting criterion, if alphabetic sorting by
394 # name is undesirable. Use nil if you don't need
395 # this, but want to set a callback function.
396 # mpprop ... property path of MP node where the file name should
398 # callback ... function that's called after a new entry was chosen,
399 # with these arguments:
401 # callback(<number>, <name>, <sort-criterion>, <file>, <path>)
404 # aircraft.data.add("sim/model/pilot"); # autosave the pilot
405 # var pilots_dialog = gui.OverlaySelector.new("Pilots",
406 # "Aircraft/foo/Models/Pilots",
407 # "sim/model/pilot");
409 # pilots_dialog.open(); # or ... close(), or toggle()
412 var OverlaySelector = {
413 new: func(title, dir, nameprop, sortprop = nil, mpprop = nil, callback = nil) {
414 var name = "overlay-select-";
415 var data = props.globals.getNode("/sim/gui/dialogs/", 1);
416 for (var i = 1; 1; i += 1)
417 if (data.getNode(name ~ i, 0) == nil)
419 data = data.getNode(name ~= i, 1);
421 var m = Dialog.new(data.getNode("dialog", 1), "gui/dialogs/overlay-select.xml", name);
422 m.parents = [OverlaySelector, Dialog];
424 # resolve the path in FG_ROOT, and --fg-aircraft dir, etc
425 m.dir = resolvepath(dir) ~ "/";
427 var relpath = func(p) substr(p, p[0] == `/`);
428 m.nameprop = relpath(nameprop);
429 m.sortprop = relpath(sortprop or nameprop);
431 m.callback = callback;
433 m.dialog_name = name;
434 m.result = data.initNode("result", "");
435 m.listener = setlistener(m.result, func(n) m.select(n.getValue()));
437 aircraft.data.add(m.nameprop);
439 # need to reinit again, whenever the GUI is reloaded
440 m.reinit_listener = setlistener("/sim/signals/reinit-gui", func(n) m.reinit());
444 me.prop.getNode("group/text/label").setValue(me.title);
445 me.prop.getNode("group/button/binding/script").setValue('gui.Dialog.instance["' ~ me.dialog_name ~ '"].close()');
446 me.list = me.prop.getNode("list");
447 me.list.getNode("property").setValue(me.result.getPath());
450 me.select(getprop(me.nameprop) or "");
453 removelistener(me.listener);
454 removelistener(me.reinit_listener);
455 # call inherited 'del'
456 me.parents = subvec(me.parents,1);
461 var files = directory(me.dir);
463 foreach (var file; files) {
464 if (substr(file, -4) != ".xml")
466 var n = io.read_properties(me.dir ~ file);
467 var name = n.getNode(me.nameprop, 1).getValue();
468 var index = n.getNode(me.sortprop, 1).getValue();
469 if (name == nil or index == nil)
471 append(me.data, [name, index, substr(file, 0, size(file) - 4), me.dir ~ file]);
473 me.data = sort(me.data, func(a, b) num(a[1]) == nil or num(b[1]) == nil
474 ? cmp(a[1], b[1]) : a[1] - b[1]);
477 me.list.removeChildren("value");
478 forindex (var i; me.data)
479 me.list.getChild("value", i, 1).setValue(me.data[i][0]);
482 var last = me.current;
483 me.current = math.mod(index, size(me.data));
484 io.read_properties(me.data[me.current][3], props.globals);
485 if (last != me.current and me.callback != nil)
486 call(me.callback, [me.current] ~ me.data[me.current], me);
487 if (me.mpprop != nil)
488 setprop(me.mpprop, me.data[me.current][2]);
491 forindex (var i; me.data)
492 if (me.data[i][0] == name)
496 me.set(me.current + 1);
499 me.set(me.current - 1);
505 # FileSelector class (derived from Dialog class).
507 # SYNOPSIS: FileSelector.new(<callback>, <title>, <button> [, <pattern> [, <dir> [, <file> [, <dotfiles>]]]])
509 # callback ... callback function that gets return value as first argument
510 # title ... dialog title
511 # button ... button text (should say "Save", "Load", etc. and not just "OK")
512 # pattern ... array with shell pattern or nil (which is equivalent to "*")
513 # dir ... starting dir ($FG_ROOT if unset)
514 # file ... pre-selected default file name
515 # dotfiles ... flag that decides whether UNIX dotfiles should be shown (1) or not (0)
519 # var report = func(n) { print("file ", n.getValue(), " selected") }
520 # var selector = gui.FileSelector.new(
521 # report, # callback function
522 # "Save Flight", # dialog title
523 # "Save", # button text
524 # ["*.sav", "*.xml"], # pattern for displayed files
525 # "/tmp", # start dir
526 # "flight.sav"); # default file name
530 # selector.set_title("Save Another Flight");
534 new: func(callback, title, button, pattern = nil, dir = "", file = "", dotfiles = 0, show_files=1) {
537 var usage = gui.FILE_DIALOG_OPEN_FILE;
539 usage = gui.FILE_DIALOG_CHOOSE_DIR;
540 } else if (button == 'Save') {
541 # nasty, should make this explicit
542 usage = gui.FILE_DIALOG_SAVE_FILE;
545 m = { parents:[FileSelector],
546 _inner: gui._createFileDialog(usage)};
549 m.set_button(button);
550 m.set_directory(dir);
552 m.set_dotfiles(dotfiles);
553 m.set_pattern(pattern);
555 m._inner.setCallback(func (path) {
556 var node = props.Node.new();
563 # setters only take effect after the next call to open()
564 set_title: func(title) { me._inner.title = title },
565 set_button: func(button) { me._inner.button = button },
566 set_directory: func(dir) { me._inner.directory = directory },
567 set_file: func(file) { me._inner.placeholder = file },
568 set_dotfiles: func(dot) { me._inner.show_hidden = dot },
569 set_pattern: func(pattern) { me._inner.pattern = (pattern == nil) ? [] : pattern },
571 open: func() { me._inner.open(); },
572 close: func() { me._inner.close(); },
581 # DirSelector - convenience "class" (indeed using a reconfigured FileSelector)
584 new: func(callback, title, button, dir = "") {
585 return FileSelector.new(callback, title, button, nil, dir, "", 0, 0);
590 # Save/load flight menu functions.
592 var save_flight_sel = nil;
593 var save_flight = func {
594 foreach (var n; props.globals.getNode("/sim/presets").getChildren())
595 n.setAttribute("archive", 1);
596 var save = func(n) fgcommand("save", props.Node.new({ file: n.getValue() }));
597 if (save_flight_sel == nil)
598 save_flight_sel = FileSelector.new(save, "Save Flight", "Save",
599 ["*.sav"], getprop("/sim/fg-home"), "flight.sav");
600 save_flight_sel.open();
604 var load_flight_sel = nil;
605 var load_flight = func {
607 fgcommand("load", props.Node.new({ file: n.getValue() }));
608 fgcommand("presets-commit");
610 if (load_flight_sel == nil)
611 load_flight_sel = FileSelector.new(load, "Load Flight", "Load",
612 ["*.sav"], getprop("/sim/fg-home"), "flight.sav");
613 load_flight_sel.open();
617 # Screen-shot directory menu function
619 var set_screenshotdir_sel = nil;
620 var set_screenshotdir = func {
621 if (set_screenshotdir_sel == nil)
622 set_screenshotdir_sel = gui.DirSelector.new(
623 func(result) { setprop("/sim/paths/screenshot-dir", result.getValue()); },
624 "Select Screenshot Directory", "Ok", getprop("/sim/paths/screenshot-dir"));
625 set_screenshotdir_sel.open();
629 # Open property browser with given target path.
631 var property_browser = func(dir = nil) {
634 elsif (isa(dir, props.Node))
636 var dlgname = "property-browser";
637 foreach (var module; keys(globals))
638 if (find("__dlg:" ~ dlgname, module) == 0)
639 return globals[module].clone(dir);
641 setprop("/sim/gui/dialogs/" ~ dlgname ~ "/last", dir);
642 fgcommand("dialog-show", props.Node.new({"dialog-name": dlgname}));
647 # Open one property browser per /browser[] property, where each contains
648 # the target path. On the command line use --prop:browser=orientation
651 foreach (var b; props.globals.getChildren("browser"))
652 if ((var browser = b.getValue()) != nil)
653 foreach (var path; split(",", browser))
655 property_browser(string.trim(path));
657 props.globals.removeChildren("browser");
662 # Apply whole dialog or list of widgets. This copies the widgets'
663 # visible contents to the respective <property>.
665 var dialog_apply = func(dialog, objects...) {
666 var n = props.Node.new({ "dialog-name": dialog });
668 return fgcommand("dialog-apply", n);
670 var name = n.getNode("object-name", 1);
671 foreach (var o; objects) {
673 fgcommand("dialog-apply", n);
679 # Update whole dialog or list of widgets. This makes the widgets
680 # adopt and display the value of their <property>.
682 var dialog_update = func(dialog, objects...) {
683 var n = props.Node.new({ "dialog-name": dialog });
685 return fgcommand("dialog-update", n);
687 var name = n.getNode("object-name", 1);
688 foreach (var o; objects) {
690 fgcommand("dialog-update", n);
696 # Searches a dialog tree for widgets with a particular <name> entry and
697 # sets their <enabled> flag.
699 var enable_widgets = func(node, name, enable = 1) {
700 foreach (var n; node.getChildren())
701 enable_widgets(n, name, enable);
702 if ((var n = node.getNode("name")) != nil and n.getValue() == name)
703 node.getNode("enabled", 1).setBoolValue(enable);
708 ########################################################################
710 ########################################################################
712 var nextStyle = func {
713 var curr = getprop("/sim/gui/current-style");
714 var styles = props.globals.getNode("/sim/gui").getChildren("style");
715 forindex (var i; styles)
716 if (styles[i].getIndex() == curr)
718 if ((i += 1) >= size(styles))
720 setprop("/sim/gui/current-style", styles[i].getIndex());
721 fgcommand("gui-redraw");
725 ########################################################################
727 ########################################################################
731 var setWeight = func(wgt, opt) {
732 var lbs = opt.getNode("lbs", 1).getValue();
733 wgt.getNode("weight-lb", 1).setValue(lbs);
735 # Weights can have "tank" indices which set the capacity of the
736 # corresponding tank. This code should probably be moved to
737 # something like fuel.setTankCap(tank, gals)...
738 if(wgt.getNode("tank",0) == nil) { return 0; }
739 var ti = wgt.getNode("tank").getValue();
740 var gn = opt.getNode("gals");
741 var gals = gn == nil ? 0 : gn.getValue();
742 var tn = props.globals.getNode("consumables/fuel/tank["~ti~"]", 1);
743 var ppg = tn.getNode("density-ppg", 1).getValue();
744 var lbs = gals * ppg;
745 var curr = tn.getNode("level-gal_us", 1).getValue();
746 curr = curr > gals ? gals : curr;
747 tn.getNode("capacity-gal_us", 1).setValue(gals);
748 tn.getNode("level-gal_us", 1).setValue(curr);
749 tn.getNode("level-lbs", 1).setValue(curr * ppg);
753 # Checks the /sim/weight[n]/{selected|opt} values and sets the
754 # appropriate weights therefrom.
755 var setWeightOpts = func {
757 foreach(var w; props.globals.getNode("sim").getChildren("weight")) {
758 var selected = w.getNode("selected");
759 if(selected != nil) {
760 foreach(var opt; w.getChildren("opt")) {
761 if(opt.getNode("name", 1).getValue() == selected.getValue()) {
762 if(setWeight(w, opt)) { tankchange = 1; }
770 # Run it at startup and on reset to make sure the tank settings are correct
771 _setlistener("/sim/signals/fdm-initialized", func { settimer(setWeightOpts, 0) });
772 _setlistener("/sim/signals/reinit", func(n) { props._getValue(n, []) or setWeightOpts() });
775 # Called from the F&W dialog when the user selects a weight option
776 var weightChangeHandler = func {
777 var tankchanged = setWeightOpts();
779 # This is unfortunate. Changing tanks means that the list of
780 # tanks selected and their slider bounds must change, but our GUI
781 # isn't dynamic in that way. The only way to get the changes on
782 # screen is to pop it down and recreate it.
784 var p = props.Node.new({"dialog-name": "WeightAndFuel"});
785 fgcommand("dialog-close", p);
793 # Dynamically generates a weight & fuel configuration dialog specific to
796 var showWeightDialog = func {
797 var name = "WeightAndFuel";
798 var title = "Weight and Fuel Settings";
801 # General Dialog Structure
803 dialog[name] = Widget.new();
804 dialog[name].set("name", name);
805 dialog[name].set("layout", "vbox");
807 var header = dialog[name].addChild("group");
808 header.set("layout", "hbox");
809 header.addChild("empty").set("stretch", "1");
810 header.addChild("text").set("label", title);
811 header.addChild("empty").set("stretch", "1");
812 var w = header.addChild("button");
813 w.set("pref-width", 16);
814 w.set("pref-height", 16);
817 w.setBinding("dialog-close");
819 dialog[name].addChild("hrule");
821 if (fdm != "yasim" and fdm != "jsb") {
822 var msg = dialog[name].addChild("text");
823 msg.set("label", "Not supported for this aircraft");
824 var cancel = dialog[name].addChild("button");
825 cancel.set("key", "Esc");
826 cancel.set("legend", "Cancel");
827 cancel.setBinding("dialog-close");
828 fgcommand("dialog-new", dialog[name].prop());
833 # FDM dependent settings
836 grosswgt : "/yasim/gross-weight-lbs",
840 } elsif(fdm == "jsb") {
842 grosswgt : "/fdm/jsbsim/inertia/weight-lbs",
843 payload : "/payload",
844 cg : "/fdm/jsbsim/inertia/cg-x-in",
848 var contentArea = dialog[name].addChild("group");
849 contentArea.set("layout", "hbox");
850 contentArea.set("default-padding", 10);
852 dialog[name].addChild("empty");
854 var limits = dialog[name].addChild("group");
855 limits.set("layout", "table");
856 limits.set("halign", "center");
859 var massLimits = props.globals.getNode("/limits/mass-and-balance");
861 var tablerow = func(name, node, format ) {
863 var n = isa( node, props.Node ) ? node : massLimits.getNode( node );
864 if( n == nil ) return;
866 var label = limits.addChild("text");
867 label.set("row", row);
869 label.set("halign", "right");
870 label.set("label", name ~ ":");
872 var val = limits.addChild("text");
875 val.set("halign", "left");
876 val.set("label", "0123457890123456789");
877 val.set("format", format);
878 val.set("property", n.getPath());
884 var grossWgt = props.globals.getNode(fdmdata.grosswgt);
885 if(grossWgt != nil) {
886 tablerow("Gross Weight", grossWgt, "%.0f lb");
889 if(massLimits != nil ) {
890 tablerow("Max. Ramp Weight", "maximum-ramp-mass-lbs", "%.0f lb" );
891 tablerow("Max. Takeoff Weight", "maximum-takeoff-mass-lbs", "%.0f lb" );
892 tablerow("Max. Landing Weight", "maximum-landing-mass-lbs", "%.0f lb" );
893 tablerow("Max. Arrested Landing Weight", "maximum-arrested-landing-mass-lbs", "%.0f lb" );
894 tablerow("Max. Zero Fuel Weight", "maximum-zero-fuel-mass-lbs", "%.0f lb" );
897 if( fdmdata.cg != nil ) {
898 var n = props.globals.getNode("/limits/mass-and-balance/cg/dimension");
899 tablerow("Center of Gravity", props.globals.getNode(fdmdata.cg), "%.1f " ~ (n == nil ? "in" : n.getValue()));
902 dialog[name].addChild("hrule");
904 var buttonBar = dialog[name].addChild("group");
905 buttonBar.set("layout", "hbox");
906 buttonBar.set("default-padding", 10);
908 var close = buttonBar.addChild("button");
909 close.set("legend", "Close");
910 close.set("default", "true");
911 close.set("key", "Enter");
912 close.setBinding("dialog-close");
914 # Temporary helper function
915 var tcell = func(parent, type, row, col) {
916 var cell = parent.addChild(type);
917 cell.set("row", row);
918 cell.set("col", col);
923 # Fill in the content area
925 var fuelArea = contentArea.addChild("group");
926 fuelArea.set("layout", "vbox");
927 fuelArea.addChild("text").set("label", "Fuel Tanks");
929 var fuelTable = fuelArea.addChild("group");
930 fuelTable.set("layout", "table");
932 fuelArea.addChild("empty").set("stretch", 1);
934 tcell(fuelTable, "text", 0, 0).set("label", "Tank");
935 tcell(fuelTable, "text", 0, 3).set("label", "Pounds");
936 tcell(fuelTable, "text", 0, 4).set("label", "Gallons");
937 tcell(fuelTable, "text", 0, 5).set("label", "Fraction");
939 var tanks = props.globals.getNode("/consumables/fuel").getChildren("tank");
940 for(var i=0; i<size(tanks); i+=1) {
944 var tnode = t.getNode("name");
945 if(tnode != nil) { tname = tnode.getValue(); }
947 var tankprop = "/consumables/fuel/tank["~i~"]";
949 var cap = t.getNode("capacity-gal_us", 0);
951 # Hack, to ignore the "ghost" tanks created by the C++ code.
952 if(cap == nil ) { continue; }
953 cap = cap.getValue();
955 # Ignore tanks of capacity 0
956 if (cap == 0) { continue; }
958 var title = tcell(fuelTable, "text", i+1, 0);
959 title.set("label", tname);
960 title.set("halign", "right");
962 var selected = props.globals.initNode(tankprop ~ "/selected", 1, "BOOL");
963 if (selected.getAttribute("writable")) {
964 var sel = tcell(fuelTable, "checkbox", i+1, 1);
965 sel.set("property", tankprop ~ "/selected");
967 sel.setBinding("dialog-apply");
970 var slider = tcell(fuelTable, "slider", i+1, 2);
971 slider.set("property", tankprop ~ "/level-gal_us");
972 slider.set("live", 1);
973 slider.set("min", 0);
974 slider.set("max", cap);
975 slider.setBinding("dialog-apply");
977 var lbs = tcell(fuelTable, "text", i+1, 3);
978 lbs.set("property", tankprop ~ "/level-lbs");
979 lbs.set("label", "0123456");
980 lbs.set("format", cap < 1 ? "%.3f" : cap < 10 ? "%.2f" : "%.1f" );
981 lbs.set("halign", "right");
984 var gals = tcell(fuelTable, "text", i+1, 4);
985 gals.set("property", tankprop ~ "/level-gal_us");
986 gals.set("label", "0123456");
987 gals.set("format", cap < 1 ? "%.3f" : cap < 10 ? "%.2f" : "%.1f" );
988 gals.set("halign", "right");
991 var per = tcell(fuelTable, "text", i+1, 5);
992 per.set("property", tankprop ~ "/level-norm");
993 per.set("label", "0123456");
994 per.set("format", "%.2f");
995 per.set("halign", "right");
999 varbar = tcell(fuelTable, "hrule", size(tanks)+1, 0);
1000 varbar.set("colspan", 6);
1002 var total_label = tcell(fuelTable, "text", size(tanks)+2, 2);
1003 total_label.set("label", "Total:");
1004 total_label.set("halign", "right");
1006 var lbs = tcell(fuelTable, "text", size(tanks)+2, 3);
1007 lbs.set("property", "/consumables/fuel/total-fuel-lbs");
1008 lbs.set("label", "0123456");
1009 lbs.set("format", "%.1f" );
1010 lbs.set("halign", "right");
1013 var gals = tcell(fuelTable, "text",size(tanks) +2, 4);
1014 gals.set("property", "/consumables/fuel/total-fuel-gal_us");
1015 gals.set("label", "0123456");
1016 gals.set("format", "%.1f" );
1017 gals.set("halign", "right");
1018 gals.set("live", 1);
1020 var per = tcell(fuelTable, "text", size(tanks)+2, 5);
1021 per.set("property", "/consumables/fuel/total-fuel-norm");
1022 per.set("label", "0123456");
1023 per.set("format", "%.2f");
1024 per.set("halign", "right");
1027 var weightArea = contentArea.addChild("group");
1028 weightArea.set("layout", "vbox");
1029 weightArea.addChild("text").set("label", "Payload");
1031 var weightTable = weightArea.addChild("group");
1032 weightTable.set("layout", "table");
1034 weightArea.addChild("empty").set("stretch", 1);
1036 tcell(weightTable, "text", 0, 0).set("label", "Location");
1037 tcell(weightTable, "text", 0, 2).set("label", "Pounds");
1039 var payload_base = props.globals.getNode(fdmdata.payload);
1040 if (payload_base != nil)
1041 var wgts = payload_base.getChildren("weight");
1044 for(var i=0; i<size(wgts); i+=1) {
1046 var wname = w.getNode("name", 1).getValue();
1047 var wprop = fdmdata.payload ~ "/weight[" ~ i ~ "]";
1049 var title = tcell(weightTable, "text", i+1, 0);
1050 title.set("label", wname);
1051 title.set("halign", "right");
1053 if(w.getNode("opt") != nil) {
1054 var combo = tcell(weightTable, "combo", i+1, 1);
1055 combo.set("property", wprop ~ "/selected");
1056 combo.set("pref-width", 300);
1058 # Simple code we'd like to use:
1059 #foreach(opt; w.getChildren("opt")) {
1060 # var ent = combo.addChild("value");
1061 # ent.prop().setValue(opt.getNode("name", 1).getValue());
1064 # More complicated workaround to move the "current" item
1065 # into the first slot, because dialog.cxx doesn't set the
1066 # selected item in the combo box.
1068 var curr = w.getNode("selected");
1069 curr = curr == nil ? "" : curr.getValue();
1070 foreach(opt; w.getChildren("opt")) {
1071 append(opts, opt.getNode("name", 1).getValue());
1073 forindex(oi; opts) {
1074 if(opts[oi] == curr) {
1081 foreach(opt; opts) {
1082 combo.addChild("value").prop().setValue(opt);
1085 combo.setBinding("dialog-apply");
1086 combo.setBinding("nasal", "gui.weightChangeHandler()");
1088 var slider = tcell(weightTable, "slider", i+1, 1);
1089 slider.set("property", wprop ~ "/weight-lb");
1090 var min = w.getNode("min-lb", 1).getValue();
1091 var max = w.getNode("max-lb", 1).getValue();
1092 slider.set("min", min != nil ? min : 0);
1093 slider.set("max", max != nil ? max : 100);
1094 slider.set("live", 1);
1095 slider.setBinding("dialog-apply");
1098 var lbs = tcell(weightTable, "text", i+1, 2);
1099 lbs.set("property", wprop ~ "/weight-lb");
1100 lbs.set("label", "0123456");
1101 lbs.set("format", "%.0f");
1105 # All done: pop it up
1106 fgcommand("dialog-new", dialog[name].prop());
1114 # Dynamically generates a dialog from a help node.
1116 # gui.showHelpDialog([<path> [, toggle]])
1118 # path ... path to help node
1119 # toggle ... decides if an already open dialog should be closed
1120 # (useful when calling the dialog from a key binding; default: 0)
1124 # each of <title>, <key>, <line>, <text> is optional; uses
1125 # "/sim/description" or "/sim/aircraft" if <title> is omitted;
1126 # only the first <text> is displayed
1130 # <title>dialog title<title>
1133 # <desc>gear up/down</desc>
1136 # <line>one line</line>
1137 # <line>another line</line>
1144 var showHelpDialog = func {
1145 var node = props.globals.getNode(arg[0]);
1146 if (arg[0] == "/sim/help" and size(node.getChildren()) < 4) {
1147 node = node.getChild("common");
1150 var name = node.getNode("title", 1).getValue();
1152 name = getprop("/sim/description");
1154 name = getprop("/sim/aircraft");
1157 var toggle = size(arg) > 1 and arg[1] != nil and arg[1] > 0;
1158 if (toggle and contains(dialog, name)) {
1159 fgcommand("dialog-close", props.Node.new({ "dialog-name": name }));
1160 delete(dialog, name);
1164 dialog[name] = Widget.new();
1165 dialog[name].set("layout", "vbox");
1166 dialog[name].set("default-padding", 0);
1167 dialog[name].set("name", name);
1170 var titlebar = dialog[name].addChild("group");
1171 titlebar.set("layout", "hbox");
1172 titlebar.addChild("empty").set("stretch", 1);
1173 titlebar.addChild("text").set("label", name);
1174 titlebar.addChild("empty").set("stretch", 1);
1176 var w = titlebar.addChild("button");
1177 w.set("pref-width", 16);
1178 w.set("pref-height", 16);
1179 w.set("legend", "");
1180 w.set("default", 1);
1181 w.set("key", "esc");
1182 w.setBinding("nasal", "delete(gui.dialog, \"" ~ name ~ "\")");
1183 w.setBinding("dialog-close");
1185 dialog[name].addChild("hrule");
1188 var keylist = dialog[name].addChild("group");
1189 keylist.set("layout", "table");
1190 keylist.set("default-padding", 2);
1191 var keydefs = node.getChildren("key");
1192 var n = size(keydefs);
1193 var row = var col = 0;
1194 foreach (var key; keydefs) {
1195 if (n >= 60 and row >= n / 3 or n >= 16 and row >= n / 2) {
1200 var w = keylist.addChild("text");
1202 w.set("col", 2 * col);
1203 w.set("halign", "right");
1204 w.set("label", " " ~ key.getNode("name").getValue());
1206 w = keylist.addChild("text");
1208 w.set("col", 2 * col + 1);
1209 w.set("halign", "left");
1210 w.set("label", "... " ~ key.getNode("desc").getValue() ~ " ");
1215 var lines = node.getChildren("line");
1217 if (size(keydefs)) {
1218 dialog[name].addChild("empty").set("pref-height", 4);
1219 dialog[name].addChild("hrule");
1220 dialog[name].addChild("empty").set("pref-height", 4);
1223 var g = dialog[name].addChild("group");
1224 g.set("layout", "vbox");
1225 g.set("default-padding", 1);
1226 foreach (var lin; lines) {
1227 foreach (var l; split("\n", lin.getValue())) {
1228 var w = g.addChild("text");
1229 w.set("halign", "left");
1230 w.set("label", " " ~ l ~ " ");
1235 # scrollable text area
1236 if (node.getNode("text") != nil) {
1237 dialog[name].set("resizable", 1);
1238 dialog[name].addChild("empty").set("pref-height", 10);
1240 var width = [640, 800, 1152][col];
1241 var height = screenHProp.getValue() - (100 + (size(keydefs) / (col + 1) + size(lines)) * 28);
1246 var w = dialog[name].addChild("textbox");
1247 w.set("padding", 4);
1248 w.set("halign", "fill");
1249 w.set("valign", "fill");
1250 w.set("stretch", "true");
1251 w.set("slider", 20);
1252 w.set("pref-width", width);
1253 w.set("pref-height", height);
1254 w.set("editable", 0);
1255 w.set("property", node.getPath() ~ "/text");
1257 dialog[name].addChild("empty").set("pref-height", 8);
1259 fgcommand("dialog-new", dialog[name].prop());
1265 title: "Development Keys",
1267 #{ name: "Ctrl-U", desc: "add 1000 ft of emergency altitude" },
1268 { name: "Shift-F3", desc: "load panel" },
1269 { name: "/", desc: "open property browser" },
1274 title: "Basic Keys",
1276 { name: "?", desc: "show/hide aircraft help dialog" },
1277 #{ name: "Tab", desc: "show/hide aircraft config dialog" },
1278 { name: "Esc", desc: "quit FlightGear" },
1279 { name: "Shift-Esc", desc: "reset FlightGear" },
1280 { name: "a/A", desc: "increase/decrease speed-up" },
1281 { name: "c", desc: "toggle 3D/2D cockpit" },
1282 { name: "Ctrl-C", desc: "toggle clickable panel hotspots" },
1283 { name: "p", desc: "pause/continue sim" },
1284 { name: "Ctrl-R", desc: "activate instant replay system" },
1285 { name: "t/T", desc: "increase/decrease warp delta" },
1286 { name: "v/V", desc: "cycle views (forward/backward)" },
1287 { name: "Ctrl-V", desc: "select cockpit view" },
1288 { name: "w/W", desc: "increase/decrease warp" },
1289 { name: "x/X", desc: "zoom in/out" },
1290 { name: "Ctrl-X", desc: "reset zoom to default" },
1291 { name: "z/Z", desc: "increase/decrease visibility" },
1292 { name: "Ctrl-Z", desc: "reset visibility to default" },
1293 { name: "'", desc: "display ATC setting dialog" },
1294 { name: "+", desc: "let ATC/instructor repeat last message" },
1295 { name: "-", desc: "open chat dialog" },
1296 { name: "_", desc: "compose chat message" },
1297 { name: "F3", desc: "capture screen" },
1298 { name: "F10", desc: "toggle menubar" },
1299 #{ name: "Shift-F1", desc: "load flight" },
1300 #{ name: "Shift-F2", desc: "save flight" },
1301 { name: "Shift-F10", desc: "cycle through GUI styles" },
1305 var common_aircraft_keys = {
1306 title: "Common Aircraft Keys",
1308 { name: "Enter", desc: "move rudder right" },
1309 { name: "0/Insert", desc: "move rudder left" },
1310 { name: "1/End", desc: "elevator trim up" },
1311 { name: "2/Down", desc: "elevator up or increase AP altitude" },
1312 { name: "3/PgDn", desc: "decr. throttle or AP autothrottle" },
1313 { name: "4/Left", desc: "move aileron left or adj. AP hdg." },
1314 { name: "5/KP5", desc: "center aileron, elev., and rudder" },
1315 { name: "6/Right", desc: "move aileron right or adj. AP hdg." },
1316 { name: "7/Home", desc: "elevator trim down" },
1317 { name: "8/Up", desc: "elevator down or decrease AP altitude" },
1318 { name: "9/PgUp", desc: "incr. throttle or AP autothrottle" },
1319 { name: "Space", desc: "PTT - Push To Talk (via VoIP)" },
1320 { name: "!/@/#/$", desc: "select engine 1/2/3/4" },
1321 { name: "b", desc: "apply all brakes" },
1322 { name: "B", desc: "toggle parking brake" },
1323 #{ name: "Ctrl-B", desc: "toggle speed brake" },
1324 { name: "g/G", desc: "gear up/down" },
1325 { name: "h", desc: "cycle HUD (head up display)" },
1326 { name: "H", desc: "cycle HUD brightness" },
1327 #{ name: "i/Shift-i", desc: "normal/alternative HUD" },
1328 #{ name: "j", desc: "decrease spoilers" },
1329 #{ name: "k", desc: "increase spoilers" },
1330 { name: "l", desc: "toggle tail-wheel lock" },
1331 { name: "m/M", desc: "mixture richer/leaner" },
1332 { name: "n/N", desc: "propeller finer/coarser" },
1333 { name: "P", desc: "toggle 2D panel" },
1334 { name: "S", desc: "swap panels" },
1335 { name: "s", desc: "fire starter on selected eng." },
1336 { name: ", .", desc: "left/right brake (comma, period)" },
1337 { name: "~", desc: "select all engines (tilde)" },
1338 { name: "[ ]", desc: "flaps up/down" },
1339 { name: "{ }", desc: "decr/incr magneto on sel. eng." },
1340 { name: "Ctrl-A", desc: "AP: toggle altitude lock" },
1341 { name: "Ctrl-G", desc: "AP: toggle glide slope lock" },
1342 { name: "Ctrl-H", desc: "AP: toggle heading lock" },
1343 { name: "Ctrl-N", desc: "AP: toggle NAV1 lock" },
1344 { name: "Ctrl-P", desc: "AP: toggle pitch hold" },
1345 { name: "Ctrl-S", desc: "AP: toggle auto-throttle" },
1346 { name: "Ctrl-T", desc: "AP: toggle terrain lock" },
1347 { name: "Ctrl-W", desc: "AP: toggle wing leveler" },
1348 { name: "F6", desc: "AP: toggle heading mode" },
1349 { name: "F11", desc: "open autopilot dialog" },
1350 { name: "F12", desc: "open radio settings dialog" },
1351 { name: "Shift-F5", desc: "scroll 2D panel down" },
1352 { name: "Shift-F6", desc: "scroll 2D panel up" },
1353 { name: "Shift-F7", desc: "scroll 2D panel left" },
1354 { name: "Shift-F8", desc: "scroll 2D panel right" },
1358 _setlistener("/sim/signals/screenshot", func {
1359 var path = getprop("/sim/paths/screenshot-last");
1360 var button = { button: { legend: "Ok", default: 1, binding: { command: "dialog-close" }}};
1361 var success= getprop("/sim/signals/screenshot");
1363 popupTip("Screenshot written to '" ~ path ~ "'", 3);
1365 popupTip("Error writing screenshot '" ~ path ~ "'", 600, button);
1369 var terrasync_stalled = 0;
1370 _setlistener("/sim/terrasync/stalled", func {
1371 var stalled = getprop("/sim/terrasync/stalled");
1372 if (stalled and !terrasync_stalled)
1374 var button = { button: { legend: "Ok", default: 1, binding: { command: "dialog-close" }}};
1375 popupTip("Scenery download stalled. Too many errors reported. See log output.", 600, button);
1377 terrasync_stalled = stalled;
1381 _setlistener("/sim/signals/fdm-initialized", func {
1382 var haveTutorials = size(props.globals.getNode("/sim/tutorials", 1).getChildren("tutorial"));
1383 gui.menuEnable("tutorial-start", haveTutorials);
1384 if (do_welcome and haveTutorials)
1385 settimer(func { setprop("/sim/messages/copilot", "Welcome aboard! Need help? Use 'Help -> Tutorials'.");}, 5.0);
1389 # load ATC chatter module on demand
1390 setprop("/nasal/atc-chatter/enabled", getprop("/sim/sound/chatter/enabled"));
1391 _setlistener("/sim/sound/chatter/enabled", func {
1392 setprop("/nasal/atc-chatter/enabled", getprop("/sim/sound/chatter/enabled"));
1396 # overwrite custom shader settings when quality-level is set on startup
1397 var qualityLevel = getprop("/sim/rendering/shaders/quality-level");
1398 var rembrandtOn = getprop("/sim/rendering/rembrandt/enabled");
1399 if (qualityLevel == -1) {
1400 setprop("/sim/rendering/shaders/custom-settings",1);
1402 elsif (qualityLevel != nil) {
1403 setprop("/sim/rendering/shaders/custom-settings",0);
1404 setprop("/sim/rendering/shaders/quality-level-internal",qualityLevel);
1405 if (qualityLevel == 0) {
1406 setprop("/sim/rendering/shaders/skydome",0);
1409 # overwrite custom shader settings when quality-level is set through the slider
1410 # in the Rendering Options dialog
1411 var update_shader_settings = func() {
1412 if (!getprop("/sim/rendering/shaders/custom-settings")){
1413 var qualityLvl = getprop("/sim/rendering/shaders/quality-level-internal");
1414 setprop("/sim/rendering/shaders/quality-level", qualityLvl);
1415 setprop("/sim/rendering/shaders/landmass",qualityLvl);
1416 setprop("/sim/rendering/shaders/urban",qualityLvl);
1417 setprop("/sim/rendering/shaders/water",qualityLvl);
1418 if (qualityLvl >= 3.0){
1421 setprop("/sim/rendering/shaders/model",qualityLvl);
1422 if (qualityLvl >= 1.0){
1425 setprop("/sim/rendering/shaders/contrails",qualityLvl);
1426 setprop("/sim/rendering/shaders/crop",qualityLvl);
1427 setprop("/sim/rendering/shaders/generic",qualityLvl);
1428 setprop("/sim/rendering/shaders/transition",qualityLvl);
1430 setprop("/sim/rendering/shaders/quality-level",-1);
1434 setprop("/sim/rendering/shaders/skydome",0);
1437 _setlistener("/sim/rendering/shaders/custom-settings", func { update_shader_settings() } );
1438 _setlistener("/sim/rendering/shaders/quality-level-internal", func { update_shader_settings() } );
1439 update_shader_settings();