ba10103ccd
Previously only a single directory of logos etc. were supported by OverlaySelector. Now multiple directories may be passed in. Patch from MERSPIELER.
1518 lines
53 KiB
Text
1518 lines
53 KiB
Text
##
|
|
# Pop up a "tip" dialog for a moment, then remove it. The delay in
|
|
# seconds can be specified as the second argument. The default is 4
|
|
# seconds. The third argument can be a hash with override values.
|
|
# Note that the tip dialog is a shared resource. If someone else
|
|
# comes along and wants to pop a tip up before your delay is finished,
|
|
# you lose. :)
|
|
#
|
|
var popupTip = func(label, delay = nil, override = nil, position = nil)
|
|
{
|
|
if (position == nil) {
|
|
position = {};
|
|
}
|
|
|
|
# Percent signs must be doubled because 'show-message' uses sprintf() on
|
|
# the label.
|
|
fgcommand("show-message",
|
|
props.Node.new(
|
|
{"label": string.replace(label, "%", "%%"),
|
|
"delay": delay,
|
|
"x": position['x'],
|
|
"y": position['y']}));
|
|
}
|
|
|
|
var showDialog = func(name) {
|
|
fgcommand("dialog-show", props.Node.new({ "dialog-name" : name }));
|
|
}
|
|
|
|
##
|
|
# Enable/disable named menu entry
|
|
#
|
|
var menuEnable = func(searchname, state) {
|
|
var menubar = props.globals.getNode("/sim/menubar/default");
|
|
if (menubar == nil)
|
|
return;
|
|
|
|
foreach (var menu; menubar.getChildren("menu")) {
|
|
foreach (var name; menu.getChildren("name")) {
|
|
if (name.getValue() == searchname) {
|
|
menu.getNode("enabled").setBoolValue(state);
|
|
}
|
|
}
|
|
foreach (var item; menu.getChildren("item")) {
|
|
foreach (var name; item.getChildren("name")) {
|
|
if (name.getValue() == searchname) {
|
|
item.getNode("enabled").setBoolValue(state);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
##
|
|
# Set the binding for a menu item to a Nasal script,
|
|
# typically a dialog open() command.
|
|
#
|
|
var menuBind = func(searchname, command) {
|
|
foreach (var menu; props.globals.getNode("/sim/menubar/default").getChildren("menu")) {
|
|
foreach (var item; menu.getChildren("item")) {
|
|
foreach (var name; item.getChildren("name")) {
|
|
if (name.getValue() == searchname) {
|
|
item.getNode("binding", 1).getNode("command", 1).setValue("nasal");
|
|
item.getNode("binding", 1).getNode("script", 1).setValue(command);
|
|
fgcommand("gui-redraw");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
##
|
|
# Set mouse cursor coordinates and shape (number or name), and return
|
|
# current shape (number).
|
|
#
|
|
# Example: var cursor = gui.setCursor();
|
|
# gui.setCursor(nil, nil, "wait");
|
|
#
|
|
var setCursor = func(x = nil, y = nil, cursor = nil) {
|
|
var args = props.Node.new();
|
|
if (x != nil) args.getNode("x", 1).setIntValue(x);
|
|
if (y != nil) args.getNode("y", 1).setIntValue(y);
|
|
if (cursor != nil) {
|
|
if (num(cursor) == nil)
|
|
cursor = cursor_types[cursor];
|
|
if (cursor == nil)
|
|
die("cursor must be one of: " ~ string.join(", ", keys(cursor_types)));
|
|
setprop("/sim/mouse/hide-cursor", cursor);
|
|
args.getNode("cursor", 1).setIntValue(cursor);
|
|
}
|
|
fgcommand("set-cursor", args);
|
|
return args.getValue("cursor");
|
|
}
|
|
|
|
##
|
|
# Supported mouse cursor types.
|
|
#
|
|
var cursor_types = { none: 0, pointer: 1, wait: 2, crosshair: 3, leftright: 4,
|
|
topside: 5, bottomside: 6, leftside: 7, rightside: 8,
|
|
topleft: 9, topright: 10, bottomleft: 11, bottomright: 12,
|
|
};
|
|
|
|
##
|
|
# Find a GUI element by given name.
|
|
# dialog: dialog root property.
|
|
# name: name of GUI element to be searched.
|
|
# Returns GUI element when found, nil otherwise.
|
|
#
|
|
var findElementByName = func(dialog,name) {
|
|
foreach( var child; dialog.getChildren() ) {
|
|
var n = child.getNode( "name" );
|
|
if( n != nil and n.getValue() == name )
|
|
return child;
|
|
var f = findElementByName(child, name);
|
|
if( f != nil ) return f;
|
|
}
|
|
return nil;
|
|
};
|
|
|
|
|
|
########################################################################
|
|
# Private Stuff:
|
|
########################################################################
|
|
|
|
##
|
|
# Initialize property nodes via a timer, to insure the props module is
|
|
# loaded. See notes in view.nas. Simply cache the screen height
|
|
# property and the argument for the "dialog-show" command. This
|
|
# probably isn't really needed...
|
|
#
|
|
var fdm = getprop("/sim/flight-model");
|
|
var screenHProp = nil;
|
|
var autopilotDisableProps = [
|
|
"/autopilot/hide-menu",
|
|
"/autopilot/KAP140/locks",
|
|
"/autopilot/CENTURYIIB/locks",
|
|
"/autopilot/CENTURYIII/locks"
|
|
];
|
|
|
|
##
|
|
# Show/hide the fps display dialog.
|
|
#
|
|
var fpsDisplay = func(n) {
|
|
var w = isa(n, props.Node) ? n.getValue() : n;
|
|
fgcommand(w ? "dialog-show" : "dialog-close", props.Node.new({"dialog-name": "fps"}));
|
|
}
|
|
var latencyDisplay = func(n) {
|
|
var w = isa(n, props.Node) ? n.getValue() : n;
|
|
fgcommand(w ? "dialog-show" : "dialog-close", props.Node.new({"dialog-name": "frame-latency"}));
|
|
}
|
|
|
|
##
|
|
# Pop down the tip dialog, if it is visible.
|
|
#
|
|
var popdown = func { fgcommand("clear-message", props.Node.new({"id": canvas.tooltip.getTooltipId()})); }
|
|
|
|
# Marker for the "current" timer. This value gets stored in the
|
|
# closure of the timer function, and is used to check that there
|
|
# hasn't been a more recent timer set that should override.
|
|
var currTimer = 0;
|
|
|
|
########################################################################
|
|
# Widgets & Layout Management
|
|
########################################################################
|
|
|
|
##
|
|
# A "widget" class that wraps a property node. It provides useful
|
|
# helper methods that are difficult or tedious with the raw property
|
|
# API. Note especially the slightly tricky addChild() method.
|
|
#
|
|
var Widget = {
|
|
set : func(name, val) { me.node.getNode(name, 1).setValue(val); },
|
|
prop : func { return me.node; },
|
|
new : func { return { parents : [Widget], node : props.Node.new() } },
|
|
addChild : func(type) {
|
|
var idx = size(me.node.getChildren(type));
|
|
var name = type ~ "[" ~ idx ~ "]";
|
|
var newnode = me.node.getNode(name, 1);
|
|
return { parents : [Widget], node : newnode };
|
|
},
|
|
setColor : func(r, g, b, a = 1) {
|
|
me.node.setValues({ color : { red:r, green:g, blue:b, alpha:a } });
|
|
},
|
|
setFont : func(n, s = 13, t = 0) {
|
|
me.node.setValues({ font : { name:n, "size":s, slant:t } });
|
|
},
|
|
setBinding : func(cmd, carg = nil) {
|
|
var idx = size(me.node.getChildren("binding"));
|
|
var node = me.node.getChild("binding", idx, 1);
|
|
node.getNode("command", 1).setValue(cmd);
|
|
if (cmd == "nasal") {
|
|
node.getNode("script", 1).setValue(carg);
|
|
} elsif (carg != nil and (cmd == "dialog-apply" or cmd == "dialog-update")) {
|
|
node.getNode("object-name", 1).setValue(carg);
|
|
}
|
|
},
|
|
};
|
|
|
|
|
|
##
|
|
# Dialog class. Maintains one XML dialog.
|
|
#
|
|
# SYNOPSIS:
|
|
# (B) Dialog.new(<dialog-name>); ... use dialog from $FG_ROOT/gui/dialogs/
|
|
#
|
|
# (A) Dialog.new(<prop>, <path> [, <dialog-name>]);
|
|
# ... load aircraft specific dialog from
|
|
# <path> under property <prop> and under
|
|
# name <dialog-name>; if no name is given,
|
|
# then it's taken from the XML dialog
|
|
#
|
|
# prop ... target node (name must be "dialog")
|
|
# path ... file path relative to $FG_ROOT
|
|
# dialog-name ... dialog <name> of dialog in $FG_ROOT/gui/dialogs/
|
|
#
|
|
# EXAMPLES:
|
|
#
|
|
# var dlg = gui.Dialog.new("/sim/gui/dialogs/foo-config/dialog",
|
|
# "Aircraft/foo/foo_config.xml");
|
|
# dlg.open();
|
|
# dlg.close();
|
|
#
|
|
# var livery_dialog = gui.Dialog.new("livery-select");
|
|
# livery_dialog.toggle();
|
|
#
|
|
var Dialog = {
|
|
instance: {},
|
|
new: func(prop, path = nil, name = nil) {
|
|
var m = { parents: [Dialog] };
|
|
m.state = 0;
|
|
m.listener = nil;
|
|
if (path == nil) { # global dialog in $FG_ROOT/gui/dialogs/
|
|
m.name = prop;
|
|
m.prop = props.Node.new({ "dialog-name" : prop });
|
|
} else { # aircraft dialog with given path
|
|
m.name = name;
|
|
m.path = path;
|
|
m.prop = isa(prop, props.Node) ? prop : props.globals.getNode(prop, 1);
|
|
if (m.prop.getName() != "dialog")
|
|
die("Dialog class: node name must end with '/dialog'");
|
|
|
|
m.listener = setlistener("/sim/signals/reinit-gui", func m.load(), 1);
|
|
}
|
|
return Dialog.instance[m.name] = m;
|
|
},
|
|
del: func {
|
|
if (me.listener != nil)
|
|
removelistener(me.listener);
|
|
},
|
|
# doesn't need to be called explicitly, but can be used to force a reload
|
|
load: func {
|
|
var state = me.state;
|
|
if (state)
|
|
me.close();
|
|
|
|
me.prop.removeChildren();
|
|
io.read_properties(me.path, me.prop);
|
|
|
|
var n = me.prop.getNode("name");
|
|
if (n == nil)
|
|
die("Dialog class: XML dialog must have <name>");
|
|
|
|
if (me.name == nil)
|
|
me.name = n.getValue();
|
|
else
|
|
n.setValue(me.name);
|
|
|
|
me.prop.getNode("dialog-name", 1).setValue(me.name);
|
|
fgcommand("dialog-new", me.prop);
|
|
if (state)
|
|
me.open();
|
|
},
|
|
# allows access to dialog-embedded Nasal variables/functions
|
|
namespace: func {
|
|
var ns = "__dlg:" ~ me.name;
|
|
me.state and contains(globals, ns) ? globals[ns] : nil;
|
|
},
|
|
open: func {
|
|
fgcommand("dialog-show", me.prop);
|
|
me.state = 1;
|
|
},
|
|
close: func {
|
|
fgcommand("dialog-close", me.prop);
|
|
me.state = 0;
|
|
},
|
|
toggle: func {
|
|
me.state ? me.close() : me.open();
|
|
},
|
|
is_open: func {
|
|
me.state;
|
|
},
|
|
};
|
|
|
|
|
|
##
|
|
# Overlay selector. Displays a list of overlay XML files and copies the
|
|
# chosen one to the property tree. The class allows to select liveries,
|
|
# insignia, decals, variants, etc. Usually the overlay properties are
|
|
# fed to "select" and "material" animations.
|
|
#
|
|
# SYNOPSIS:
|
|
# OverlaySelector.new(<title>, <dir>, <nameprop> [, <sortprop> [, <mpprop> [, <callback>]]]);
|
|
#
|
|
# title ... dialog title
|
|
# dirs ... directory or vector of directories where to find the XML overlay files,
|
|
# relative to FG_ROOT
|
|
# nameprop ... property in an overlay file that contains the name
|
|
# The result is written to this place in the
|
|
# property tree.
|
|
# sortprop ... property in an overlay file that should be used
|
|
# as sorting criterion, if alphabetic sorting by
|
|
# name is undesirable. Use nil if you don't need
|
|
# this, but want to set a callback function.
|
|
# mpprop ... property path of MP node where the file name should
|
|
# be written to
|
|
# callback ... function that's called after a new entry was chosen,
|
|
# with these arguments:
|
|
#
|
|
# callback(<number>, <name>, <sort-criterion>, <file>, <path>)
|
|
#
|
|
# EXAMPLE:
|
|
# aircraft.data.add("sim/model/pilot"); # autosave the pilot
|
|
# var pilots_dialog = gui.OverlaySelector.new("Pilots",
|
|
# "Aircraft/foo/Models/Pilots",
|
|
# "sim/model/pilot");
|
|
#
|
|
# pilots_dialog.open(); # or ... close(), or toggle()
|
|
#
|
|
#
|
|
var OverlaySelector = {
|
|
new: func(title, dirs, nameprop, sortprop = nil, mpprop = nil, callback = nil) {
|
|
if (!isvec(dirs)) {
|
|
dirs = [dirs];
|
|
}
|
|
var name = "overlay-select-";
|
|
var data = props.globals.getNode("/sim/gui/dialogs/", 1);
|
|
for (var i = 1; 1; i += 1)
|
|
if (data.getNode(name ~ i, 0) == nil)
|
|
break;
|
|
data = data.getNode(name ~= i, 1);
|
|
|
|
var m = Dialog.new(data.getNode("dialog", 1), "gui/dialogs/overlay-select.xml", name);
|
|
m.parents = [OverlaySelector, Dialog];
|
|
|
|
# resolve the path in FG_ROOT, and --fg-aircraft dir, etc
|
|
m.dirs = [];
|
|
for (var i = 0; i < size(dirs); i += 1) {
|
|
append(m.dirs, os.path.new(resolvepath(dirs[i])));
|
|
}
|
|
|
|
var relpath = func(p) substr(p, p[0] == `/`);
|
|
m.nameprop = relpath(nameprop);
|
|
m.sortprop = relpath(sortprop or nameprop);
|
|
m.mpprop = mpprop;
|
|
m.callback = callback;
|
|
m.title = title;
|
|
m.dialog_name = name;
|
|
m.result = data.initNode("result", "");
|
|
m.listener = setlistener(m.result, func(n) m.select(n.getValue()));
|
|
if (m.mpprop != nil)
|
|
aircraft.data.add(m.nameprop);
|
|
m.reinit();
|
|
# need to reinit again, whenever the GUI is reloaded
|
|
m.reinit_listener = setlistener("/sim/signals/reinit-gui", func(n) m.reinit());
|
|
return m;
|
|
},
|
|
reinit: func {
|
|
me.prop.getNode("group/text/label").setValue(me.title);
|
|
me.prop.getNode("group/button/binding/script").setValue('gui.Dialog.instance["' ~ me.dialog_name ~ '"].close()');
|
|
me.list = me.prop.getNode("list");
|
|
me.list.getNode("property").setValue(me.result.getPath());
|
|
me.rescan();
|
|
me.current = -1;
|
|
me.select(getprop(me.nameprop) or "");
|
|
},
|
|
del: func {
|
|
removelistener(me.listener);
|
|
removelistener(me.reinit_listener);
|
|
# call inherited 'del'
|
|
me.parents = subvec(me.parents,1);
|
|
me.del();
|
|
},
|
|
rescan: func {
|
|
me.data = [];
|
|
var files = [];
|
|
foreach (var dir; me.dirs) {
|
|
files = directory(dir.realpath);
|
|
|
|
if (size(files)) {
|
|
foreach (var file; files) {
|
|
path = os.path.new(dir.realpath);
|
|
path.append(file);
|
|
if (path.lower_extension != "xml")
|
|
continue;
|
|
var n = io.read_properties(path.realpath);
|
|
var name = n.getNode(me.nameprop, 1).getValue();
|
|
var index = n.getNode(me.sortprop, 1).getValue();
|
|
if (name == nil or index == nil)
|
|
continue;
|
|
append(me.data, [name, index, substr(file, 0, size(file) - 4), path.realpath]);
|
|
}
|
|
}
|
|
}
|
|
me.data = sort(me.data, func(a, b) num(a[1]) == nil or num(b[1]) == nil
|
|
? cmp(a[1], b[1]) : a[1] - b[1]);
|
|
|
|
me.list.removeChildren("value");
|
|
forindex (var i; me.data)
|
|
me.list.getChild("value", i, 1).setValue(me.data[i][0]);
|
|
},
|
|
set: func(index) {
|
|
var last = me.current;
|
|
me.current = math.mod(index, size(me.data));
|
|
io.read_properties(me.data[me.current][3], props.globals);
|
|
if (last != me.current and me.callback != nil)
|
|
call(me.callback, [me.current] ~ me.data[me.current], me);
|
|
if (me.mpprop != nil)
|
|
setprop(me.mpprop, me.data[me.current][2]);
|
|
},
|
|
select: func(name) {
|
|
forindex (var i; me.data)
|
|
if (me.data[i][0] == name)
|
|
me.set(i);
|
|
},
|
|
next: func {
|
|
me.set(me.current + 1);
|
|
},
|
|
previous: func {
|
|
me.set(me.current - 1);
|
|
},
|
|
};
|
|
|
|
|
|
##
|
|
# FileSelector class (derived from Dialog class).
|
|
#
|
|
# SYNOPSIS: FileSelector.new(<callback>, <title>, <button> [, <pattern> [, <dir> [, <file> [, <dotfiles>]]]])
|
|
#
|
|
# callback ... callback function that gets return value as first argument
|
|
# title ... dialog title
|
|
# button ... button text (should say "Save", "Load", etc. and not just "OK")
|
|
# pattern ... array with shell pattern or nil (which is equivalent to "*")
|
|
# dir ... starting dir ($FG_ROOT if unset)
|
|
# file ... pre-selected default file name
|
|
# dotfiles ... flag that decides whether UNIX dotfiles should be shown (1) or not (0)
|
|
#
|
|
# EXAMPLE:
|
|
#
|
|
# var report = func(n) { print("file ", n.getValue(), " selected") }
|
|
# var selector = gui.FileSelector.new(
|
|
# report, # callback function
|
|
# "Save Flight", # dialog title
|
|
# "Save", # button text
|
|
# ["*.sav", "*.xml"], # pattern for displayed files
|
|
# "/tmp", # start dir
|
|
# "flight.sav"); # default file name
|
|
# selector.open();
|
|
#
|
|
# selector.close();
|
|
# selector.set_title("Save Another Flight");
|
|
# selector.open();
|
|
#
|
|
var FileSelector = {
|
|
new: func(callback, title, button, pattern = nil, dir = "", file = "", dotfiles = 0, show_files=1) {
|
|
|
|
|
|
var usage = gui.FILE_DIALOG_OPEN_FILE;
|
|
if (!show_files) {
|
|
usage = gui.FILE_DIALOG_CHOOSE_DIR;
|
|
} else if (button == 'Save') {
|
|
# nasty, should make this explicit
|
|
usage = gui.FILE_DIALOG_SAVE_FILE;
|
|
}
|
|
|
|
m = { parents:[FileSelector],
|
|
_inner: gui._createFileDialog(usage)};
|
|
|
|
m.set_title(title);
|
|
m.set_button(button);
|
|
m.set_directory(dir);
|
|
m.set_file(file);
|
|
m.set_dotfiles(dotfiles);
|
|
m.set_pattern(pattern);
|
|
|
|
m._inner.setCallback(func (path) {
|
|
var node = props.Node.new();
|
|
node.setValue(path);
|
|
callback(node);
|
|
} );
|
|
|
|
return m;
|
|
},
|
|
# setters only take effect after the next call to open()
|
|
set_title: func(title) { me._inner.title = title },
|
|
set_button: func(button) { me._inner.button = button },
|
|
set_directory: func(dir) { me._inner.directory = dir },
|
|
set_file: func(file) { me._inner.placeholder = file },
|
|
set_dotfiles: func(dot) { me._inner.show_hidden = dot },
|
|
set_pattern: func(pattern) { me._inner.pattern = (pattern == nil) ? [] : pattern },
|
|
|
|
open: func() { me._inner.open(); },
|
|
close: func() { me._inner.close(); },
|
|
|
|
del: func {
|
|
me._inner.close();
|
|
me._inner = nil;
|
|
},
|
|
};
|
|
|
|
##
|
|
# DirSelector - convenience "class" (indeed using a reconfigured FileSelector)
|
|
#
|
|
var DirSelector = {
|
|
new: func(callback, title, button, dir = "") {
|
|
return FileSelector.new(callback, title, button, nil, dir, "", 0, 0);
|
|
}
|
|
};
|
|
|
|
##
|
|
# Save/load flight menu functions.
|
|
#
|
|
var save_flight_sel = nil;
|
|
var save_flight = func {
|
|
foreach (var n; props.globals.getNode("/sim/presets").getChildren())
|
|
n.setAttribute("archive", 1);
|
|
var save = func(n) fgcommand("save", props.Node.new({ file: n.getValue() }));
|
|
if (save_flight_sel == nil)
|
|
save_flight_sel = FileSelector.new(save, "Save Flight", "Save",
|
|
["*.sav"], getprop("/sim/fg-home"), "flight.sav");
|
|
save_flight_sel.open();
|
|
}
|
|
|
|
|
|
var load_flight_sel = nil;
|
|
var load_flight = func {
|
|
var load = func(n) {
|
|
fgcommand("load", props.Node.new({ file: n.getValue() }));
|
|
fgcommand("reposition");
|
|
}
|
|
if (load_flight_sel == nil)
|
|
load_flight_sel = FileSelector.new(load, "Load Flight", "Load",
|
|
["*.sav"], getprop("/sim/fg-home"), "flight.sav");
|
|
load_flight_sel.open();
|
|
}
|
|
|
|
##
|
|
# Screen-shot directory menu function
|
|
#
|
|
var set_screenshotdir_sel = nil;
|
|
var set_screenshotdir = func {
|
|
if (set_screenshotdir_sel == nil)
|
|
set_screenshotdir_sel = gui.DirSelector.new(
|
|
func(result) { setprop("/sim/paths/screenshot-dir", result.getValue()); },
|
|
"Select Screenshot Directory", "Ok", getprop("/sim/paths/screenshot-dir"));
|
|
set_screenshotdir_sel.open();
|
|
}
|
|
|
|
##
|
|
# Open property browser with given target path.
|
|
#
|
|
var property_browser = func(dir = nil) {
|
|
if (dir == nil)
|
|
dir = "/";
|
|
elsif (isa(dir, props.Node))
|
|
dir = dir.getPath();
|
|
var dlgname = "property-browser";
|
|
foreach (var module; keys(globals))
|
|
if (find("__dlg:" ~ dlgname, module) == 0)
|
|
return globals[module].clone(dir);
|
|
|
|
setprop("/sim/gui/dialogs/" ~ dlgname ~ "/last", dir);
|
|
fgcommand("dialog-show", props.Node.new({"dialog-name": dlgname}));
|
|
}
|
|
|
|
|
|
##
|
|
# Open one property browser per /browser[] property, where each contains
|
|
# the target path. On the command line use --prop:browser=orientation
|
|
#
|
|
settimer(func {
|
|
foreach (var b; props.globals.getChildren("browser"))
|
|
if ((var browser = b.getValue()) != nil)
|
|
foreach (var path; split(",", browser))
|
|
if (size(path))
|
|
property_browser(string.trim(path));
|
|
|
|
props.globals.removeChildren("browser");
|
|
}, 0);
|
|
|
|
|
|
##
|
|
# Apply whole dialog or list of widgets. This copies the widgets'
|
|
# visible contents to the respective <property>.
|
|
#
|
|
var dialog_apply = func(dialog, objects...) {
|
|
var n = props.Node.new({ "dialog-name": dialog });
|
|
if (!size(objects))
|
|
return fgcommand("dialog-apply", n);
|
|
|
|
var name = n.getNode("object-name", 1);
|
|
foreach (var o; objects) {
|
|
name.setValue(o);
|
|
fgcommand("dialog-apply", n);
|
|
}
|
|
}
|
|
|
|
|
|
##
|
|
# Update whole dialog or list of widgets. This makes the widgets
|
|
# adopt and display the value of their <property>.
|
|
#
|
|
var dialog_update = func(dialog, objects...) {
|
|
var n = props.Node.new({ "dialog-name": dialog });
|
|
if (!size(objects))
|
|
return fgcommand("dialog-update", n);
|
|
|
|
var name = n.getNode("object-name", 1);
|
|
foreach (var o; objects) {
|
|
name.setValue(o);
|
|
fgcommand("dialog-update", n);
|
|
}
|
|
}
|
|
|
|
|
|
##
|
|
# Searches a dialog tree for widgets with a particular <name> entry and
|
|
# sets their <enabled> flag.
|
|
#
|
|
var enable_widgets = func(node, name, enable = 1) {
|
|
foreach (var n; node.getChildren())
|
|
enable_widgets(n, name, enable);
|
|
if ((var n = node.getNode("name")) != nil and n.getValue() == name)
|
|
node.getNode("enabled", 1).setBoolValue(enable);
|
|
}
|
|
|
|
|
|
|
|
########################################################################
|
|
# GUI theming
|
|
########################################################################
|
|
|
|
var nextStyle = func(delta=1) {
|
|
var curr = getprop("/sim/gui/current-style");
|
|
var styles = props.globals.getNode("/sim/gui").getChildren("style");
|
|
forindex (var i; styles)
|
|
if (styles[i].getIndex() == curr)
|
|
break;
|
|
i += delta;
|
|
if (i >= size(styles))
|
|
i = 0;
|
|
if (i < 0) {
|
|
i = size(styles) - 1;
|
|
}
|
|
setprop("/sim/gui/current-style", styles[i].getIndex());
|
|
fgcommand("gui-redraw");
|
|
popupTip(sprintf("GUI style %s: %s",
|
|
styles[i].getIndex(),
|
|
styles[i].getValue("name"),
|
|
));
|
|
}
|
|
|
|
|
|
########################################################################
|
|
# Dialog Boxes
|
|
########################################################################
|
|
|
|
var dialog = {};
|
|
|
|
var setWeight = func(wgt, opt) {
|
|
var lbs = opt.getNode("lbs", 1).getValue();
|
|
wgt.getNode("weight-lb", 1).setValue(lbs);
|
|
|
|
# Weights can have "tank" indices which set the capacity of the
|
|
# corresponding tank. This code should probably be moved to
|
|
# something like fuel.setTankCap(tank, gals)...
|
|
var ti = wgt.getNode("tank");
|
|
|
|
if(ti == nil or ti.getValue() == "") {
|
|
return nil;
|
|
}
|
|
ti = ti.getValue();
|
|
|
|
var gn = opt.getNode("gals");
|
|
var gals = gn == nil ? 0 : gn.getValue();
|
|
var tn = props.globals.getNode("consumables/fuel/tank["~ti~"]", 1);
|
|
var ppg = tn.getNode("density-ppg", 1).getValue();
|
|
var lbs = gals * ppg;
|
|
var curr = tn.getNode("level-gal_us", 1).getValue();
|
|
curr = curr > gals ? gals : curr;
|
|
tn.getNode("capacity-gal_us", 1).setValue(gals);
|
|
tn.getNode("level-gal_us", 1).setValue(curr);
|
|
tn.getNode("level-lbs", 1).setValue(curr * ppg);
|
|
return 1;
|
|
}
|
|
|
|
# Checks the /sim/weight[n]/{selected|opt} values and sets the
|
|
# appropriate weights therefrom.
|
|
var setWeightOpts = func {
|
|
var tankchange = 0;
|
|
var root_node = nil;
|
|
if(fdm == "yasim")
|
|
root_node = props.globals.getNode("sim");
|
|
elsif (fdm == "jsb")
|
|
root_node = props.globals.getNode("payload");
|
|
if (root_node == nil) {
|
|
print("setWeight() - not supported for ",fdm);
|
|
tankchange = nil;
|
|
}
|
|
else {
|
|
foreach (var w; root_node.getChildren("weight")) {
|
|
var selected = w.getNode("selected");
|
|
if (selected != nil) {
|
|
foreach (var opt; w.getChildren("opt")) {
|
|
if (opt.getNode("name", 1).getValue() == selected.getValue()) {
|
|
if (setWeight(w, opt)) {
|
|
tankchange = 1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return tankchange;
|
|
}
|
|
# Run it at startup and on reset to make sure the tank settings are correct
|
|
_setlistener("/sim/signals/fdm-initialized", func { settimer(setWeightOpts, 0) });
|
|
_setlistener("/sim/signals/reinit", func(n) { props._getValue(n, []) or setWeightOpts() });
|
|
|
|
|
|
# Called from the F&W dialog when the user selects a weight option
|
|
var weightChangeHandler = func {
|
|
var tankchanged = setWeightOpts();
|
|
|
|
# This is unfortunate. Changing tanks means that the list of
|
|
# tanks selected and their slider bounds must change, but our GUI
|
|
# isn't dynamic in that way. The only way to get the changes on
|
|
# screen is to pop it down and recreate it.
|
|
# 2018.3 - change to restore the current dialog as the topmost
|
|
# as otherwise the open / close will put this dialog at the
|
|
# front of the display. Requires the new logic in the core
|
|
# that maintains /sim/gui/dialogs/current-dialog
|
|
if(tankchanged) {
|
|
var current_dialog = getprop("/sim/gui/dialogs/current-dialog");
|
|
var p = props.Node.new({"dialog-name": "WeightAndFuel"});
|
|
fgcommand("dialog-close", p);
|
|
showWeightDialog();
|
|
if (current_dialog != ""){
|
|
var show_node = props.Node.new({"dialog-name": current_dialog});
|
|
debug.dump(show_node);
|
|
fgcommand("dialog-show", show_node);
|
|
}
|
|
}
|
|
}
|
|
|
|
# 2018.3 - certain aircraft (e.g. the F-15) have their own external stores dialog
|
|
# in addition to the standard one. This listener and associated code
|
|
# allow the two dialogs to remain synchronised.
|
|
|
|
var weightDialogOpen = 0;
|
|
setprop("sim/gui/dialogs/payload-reload",0);
|
|
|
|
_setlistener("sim/gui/dialogs/payload-reload", func(v){
|
|
if (weightDialogOpen)
|
|
weightChangeHandler();
|
|
});
|
|
|
|
var percentMacListener = nil;
|
|
|
|
##
|
|
# Dynamically generates a weight & fuel configuration dialog specific to
|
|
# the aircraft.
|
|
#
|
|
var weightAndFuel_x = nil;
|
|
var weightAndFuel_y = nil;
|
|
|
|
var dlg_nasal_close =
|
|
"gui.weightAndFuel_y = cmdarg().getNode(\"lasty\").getValue();" ~
|
|
"gui.weightAndFuel_x = cmdarg().getNode(\"lastx\").getValue();" ~
|
|
"gui.weightDialogOpen = 0;" ~
|
|
"if (gui.percentMacListener != nil) { " ~
|
|
"removelistener(gui.percentMacListener); " ~
|
|
"gui.percentMacListener = nil; " ~
|
|
"}";
|
|
|
|
var showWeightDialog = func {
|
|
var name = "WeightAndFuel";
|
|
# menu entry is "Fuel and Payload"
|
|
var title = "Fuel and Payload Settings";
|
|
weightDialogOpen = 1;
|
|
#
|
|
# General Dialog Structure
|
|
#
|
|
dialog[name] = Widget.new();
|
|
dialog[name].set("name", name);
|
|
dialog[name].set("layout", "vbox");
|
|
|
|
# 2018.3 - add a close method that will remember the coordinates and also clear the flag that
|
|
# indicates this dialog is visible
|
|
var nasal = dialog[name].addChild("nasal");
|
|
var nasal_close = nasal.addChild("close");
|
|
nasal_close.node.setValue(dlg_nasal_close);
|
|
|
|
# if we have an X coordinate then set both (to the previous position as recorded on close).
|
|
if (weightAndFuel_x != nil){
|
|
dialog[name].set("x", weightAndFuel_x);
|
|
dialog[name].set("y", weightAndFuel_y);
|
|
}
|
|
|
|
var header = dialog[name].addChild("group");
|
|
header.set("layout", "hbox");
|
|
header.addChild("empty").set("stretch", "1");
|
|
header.addChild("text").set("label", title);
|
|
header.addChild("empty").set("stretch", "1");
|
|
var w = header.addChild("button");
|
|
w.set("pref-width", 16);
|
|
w.set("pref-height", 16);
|
|
w.set("legend", "");
|
|
w.set("default", 0);
|
|
# "Esc" causes dialog-close
|
|
w.set("key", "Esc");
|
|
w.setBinding("dialog-close");
|
|
|
|
dialog[name].addChild("hrule");
|
|
|
|
if (fdm != "yasim" and fdm != "jsb") {
|
|
var msg = dialog[name].addChild("text");
|
|
msg.set("label", "Not supported for this aircraft");
|
|
var cancel = dialog[name].addChild("button");
|
|
cancel.set("key", "Esc");
|
|
cancel.set("legend", "Cancel");
|
|
cancel.setBinding("dialog-close");
|
|
fgcommand("dialog-new", dialog[name].prop());
|
|
showDialog(name);
|
|
return;
|
|
}
|
|
|
|
# FDM dependent settings
|
|
if(fdm == "yasim") {
|
|
var fdmdata = {
|
|
grosswgt : "/fdm/yasim/gross-weight-lbs",
|
|
payload : "/sim",
|
|
cg : "/fdm/yasim/cg-x-m",
|
|
cgMAC : "/fdm/yasim/cg-x-mac",
|
|
};
|
|
setprop("/limits/mass-and-balance/cg/dimension","m");
|
|
} elsif(fdm == "jsb") {
|
|
var fdmdata = {
|
|
grosswgt : "/fdm/jsbsim/inertia/weight-lbs",
|
|
payload : "/payload",
|
|
cg : "/fdm/jsbsim/inertia/cg-x-in",
|
|
cgMAC : nil,
|
|
};
|
|
}
|
|
|
|
var contentArea = dialog[name].addChild("group");
|
|
contentArea.set("layout", "hbox");
|
|
contentArea.set("default-padding", 10);
|
|
|
|
dialog[name].addChild("empty");
|
|
|
|
var limits = dialog[name].addChild("group");
|
|
limits.set("layout", "table");
|
|
limits.set("halign", "center");
|
|
var row = 0;
|
|
|
|
var massLimits = props.globals.getNode("/limits/mass-and-balance");
|
|
|
|
var tablerow = func(name, node, format ) {
|
|
|
|
var n = isa( node, props.Node ) ? node : massLimits.getNode( node );
|
|
if( n == nil ) return;
|
|
|
|
var label = limits.addChild("text");
|
|
label.set("row", row);
|
|
label.set("col", 0);
|
|
label.set("halign", "right");
|
|
label.set("label", name ~ ":");
|
|
|
|
var val = limits.addChild("text");
|
|
val.set("row", row);
|
|
val.set("col", 1);
|
|
val.set("halign", "left");
|
|
val.set("label", "0123457890123456789");
|
|
val.set("format", format);
|
|
val.set("property", n.getPath());
|
|
val.set("live", 1);
|
|
|
|
row += 1;
|
|
}
|
|
|
|
var grossWgt = props.globals.getNode(fdmdata.grosswgt);
|
|
if(grossWgt != nil) {
|
|
tablerow("Gross Weight", grossWgt, "%.0f lb");
|
|
}
|
|
|
|
if(massLimits != nil ) {
|
|
tablerow("Max. Ramp Weight", "maximum-ramp-mass-lbs", "%.0f lb" );
|
|
tablerow("Max. Takeoff Weight", "maximum-takeoff-mass-lbs", "%.0f lb" );
|
|
tablerow("Max. Landing Weight", "maximum-landing-mass-lbs", "%.0f lb" );
|
|
tablerow("Max. Arrested Landing Weight", "maximum-arrested-landing-mass-lbs", "%.0f lb" );
|
|
tablerow("Max. Zero Fuel Weight", "maximum-zero-fuel-mass-lbs", "%.0f lb" );
|
|
}
|
|
|
|
if( fdmdata.cg != nil ) {
|
|
var n = props.globals.getNode("/limits/mass-and-balance/cg/dimension");
|
|
tablerow("Center of Gravity", props.globals.getNode(fdmdata.cg), "%.2f " ~ (n == nil ? "in" : n.getValue()));
|
|
}
|
|
|
|
if( fdmdata.cgMAC != nil ) {
|
|
var percentMac = props.globals.getNode("/limits/mass-and-balance/cg/percent-mac");
|
|
if (percentMac == nil) {
|
|
percentMac = props.globals.initNode("/limits/mass-and-balance/cg/percent-mac", 0, "DOUBLE");
|
|
}
|
|
|
|
percentMacListener = _setlistener(fdmdata.cgMAC, func {
|
|
var value = props.globals.getNode(fdmdata.cgMAC).getDoubleValue() * 100;
|
|
percentMac.setDoubleValue(value);
|
|
}, 1, 0);
|
|
|
|
tablerow("Center of Gravity", percentMac, "%.2f%% MAC" );
|
|
}
|
|
|
|
dialog[name].addChild("hrule");
|
|
|
|
var buttonBar = dialog[name].addChild("group");
|
|
buttonBar.set("layout", "hbox");
|
|
buttonBar.set("default-padding", 10);
|
|
|
|
var close = buttonBar.addChild("button");
|
|
close.set("legend", "Close");
|
|
close.set("default", "true");
|
|
close.set("key", "Enter");
|
|
close.setBinding("dialog-close");
|
|
|
|
# Temporary helper function
|
|
var tcell = func(parent, type, row, col) {
|
|
var cell = parent.addChild(type);
|
|
cell.set("row", row);
|
|
cell.set("col", col);
|
|
return cell;
|
|
}
|
|
|
|
#
|
|
# Fill in the content area
|
|
#
|
|
var fuelArea = contentArea.addChild("group");
|
|
fuelArea.set("layout", "vbox");
|
|
fuelArea.addChild("text").set("label", "Fuel Tanks");
|
|
|
|
var fuelTable = fuelArea.addChild("group");
|
|
fuelTable.set("layout", "table");
|
|
|
|
fuelArea.addChild("empty").set("stretch", 1);
|
|
|
|
tcell(fuelTable, "text", 0, 0).set("label", "Tank");
|
|
tcell(fuelTable, "text", 0, 3).set("label", "Pounds");
|
|
tcell(fuelTable, "text", 0, 4).set("label", "Gallons");
|
|
tcell(fuelTable, "text", 0, 5).set("label", "Fraction");
|
|
|
|
var tanks = props.globals.getNode("/consumables/fuel").getChildren("tank");
|
|
for(var i=0; i<size(tanks); i+=1) {
|
|
var t = tanks[i];
|
|
var hidden=0;
|
|
var tname = i ~ "";
|
|
var hnode = t.getNode("hidden");
|
|
if(hnode != nil) { hidden = hnode.getValue(); }# Check for <hidden> property ,skip adding tank if true#
|
|
if(!hidden){
|
|
var tnode = t.getNode("name");
|
|
if(tnode != nil) { tname = tnode.getValue(); }
|
|
|
|
var tankprop = "/consumables/fuel/tank["~i~"]";
|
|
|
|
var cap = t.getNode("capacity-gal_us", 0);
|
|
|
|
# Hack, to ignore the "ghost" tanks created by the C++ code.
|
|
if(cap == nil ) { continue; }
|
|
cap = cap.getValue();
|
|
|
|
# Ignore tanks of capacity 0
|
|
if (cap == 0) { continue; }
|
|
|
|
var title = tcell(fuelTable, "text", i+1, 0);
|
|
title.set("label", tname);
|
|
title.set("halign", "right");
|
|
|
|
var selected = props.globals.initNode(tankprop ~ "/selected", 1, "BOOL");
|
|
if (selected.getAttribute("writable")) {
|
|
var sel = tcell(fuelTable, "checkbox", i+1, 1);
|
|
sel.set("property", tankprop ~ "/selected");
|
|
sel.set("live", 1);
|
|
sel.setBinding("dialog-apply");
|
|
}
|
|
|
|
var slider = tcell(fuelTable, "slider", i+1, 2);
|
|
slider.set("property", tankprop ~ "/level-gal_us");
|
|
slider.set("live", 1);
|
|
slider.set("min", 0);
|
|
slider.set("max", cap);
|
|
slider.setBinding("dialog-apply");
|
|
|
|
var lbs = tcell(fuelTable, "text", i+1, 3);
|
|
lbs.set("property", tankprop ~ "/level-lbs");
|
|
lbs.set("label", "0123456");
|
|
lbs.set("format", cap < 1 ? "%.3f" : cap < 10 ? "%.2f" : "%.1f" );
|
|
lbs.set("halign", "right");
|
|
lbs.set("live", 1);
|
|
|
|
var gals = tcell(fuelTable, "text", i+1, 4);
|
|
gals.set("property", tankprop ~ "/level-gal_us");
|
|
gals.set("label", "0123456");
|
|
gals.set("format", cap < 1 ? "%.3f" : cap < 10 ? "%.2f" : "%.1f" );
|
|
gals.set("halign", "right");
|
|
gals.set("live", 1);
|
|
|
|
var per = tcell(fuelTable, "text", i+1, 5);
|
|
per.set("property", tankprop ~ "/level-norm");
|
|
per.set("label", "0123456");
|
|
per.set("format", "%.2f");
|
|
per.set("halign", "right");
|
|
per.set("live", 1);
|
|
}
|
|
}
|
|
|
|
varbar = tcell(fuelTable, "hrule", size(tanks)+1, 0);
|
|
varbar.set("colspan", 6);
|
|
|
|
var total_label = tcell(fuelTable, "text", size(tanks)+2, 2);
|
|
total_label.set("label", "Total:");
|
|
total_label.set("halign", "right");
|
|
|
|
var lbs = tcell(fuelTable, "text", size(tanks)+2, 3);
|
|
lbs.set("property", "/consumables/fuel/total-fuel-lbs");
|
|
lbs.set("label", "0123456");
|
|
lbs.set("format", "%.1f" );
|
|
lbs.set("halign", "right");
|
|
lbs.set("live", 1);
|
|
|
|
var gals = tcell(fuelTable, "text",size(tanks) +2, 4);
|
|
gals.set("property", "/consumables/fuel/total-fuel-gal_us");
|
|
gals.set("label", "0123456");
|
|
gals.set("format", "%.1f" );
|
|
gals.set("halign", "right");
|
|
gals.set("live", 1);
|
|
|
|
var per = tcell(fuelTable, "text", size(tanks)+2, 5);
|
|
per.set("property", "/consumables/fuel/total-fuel-norm");
|
|
per.set("label", "0123456");
|
|
per.set("format", "%.2f");
|
|
per.set("halign", "right");
|
|
per.set("live", 1);
|
|
|
|
var weightArea = contentArea.addChild("group");
|
|
weightArea.set("layout", "vbox");
|
|
weightArea.addChild("text").set("label", "Payload");
|
|
|
|
var weightTable = weightArea.addChild("group");
|
|
weightTable.set("layout", "table");
|
|
|
|
weightArea.addChild("empty").set("stretch", 1);
|
|
|
|
tcell(weightTable, "text", 0, 0).set("label", "Location");
|
|
tcell(weightTable, "text", 0, 2).set("label", "Pounds");
|
|
|
|
var payload_base = props.globals.getNode(fdmdata.payload);
|
|
if (payload_base != nil)
|
|
var wgts = payload_base.getChildren("weight");
|
|
else
|
|
var wgts = [];
|
|
for(var i=0; i<size(wgts); i+=1) {
|
|
var w = wgts[i];
|
|
var wname = w.getNode("name", 1).getValue() or "";
|
|
var wprop = fdmdata.payload ~ "/weight[" ~ i ~ "]";
|
|
|
|
var title = tcell(weightTable, "text", i+1, 0);
|
|
title.set("label", wname);
|
|
title.set("halign", "right");
|
|
|
|
if(w.getNode("opt") != nil) {
|
|
var combo = tcell(weightTable, "combo", i+1, 1);
|
|
combo.set("property", wprop ~ "/selected");
|
|
combo.set("pref-width", 300);
|
|
|
|
# Simple code we'd like to use:
|
|
#foreach(opt; w.getChildren("opt")) {
|
|
# var ent = combo.addChild("value");
|
|
# ent.prop().setValue(opt.getNode("name", 1).getValue());
|
|
#}
|
|
|
|
# More complicated workaround to move the "current" item
|
|
# into the first slot, because dialog.cxx doesn't set the
|
|
# selected item in the combo box.
|
|
var opts = [];
|
|
var curr = w.getNode("selected");
|
|
curr = curr == nil ? "" : curr.getValue();
|
|
foreach(opt; w.getChildren("opt")) {
|
|
append(opts, opt.getNode("name", 1).getValue());
|
|
}
|
|
forindex(oi; opts) {
|
|
if(opts[oi] == curr) {
|
|
var tmp = opts[0];
|
|
opts[0] = opts[oi];
|
|
opts[oi] = tmp;
|
|
break;
|
|
}
|
|
}
|
|
foreach(opt; opts) {
|
|
combo.addChild("value").prop().setValue(opt);
|
|
}
|
|
|
|
combo.setBinding("dialog-apply");
|
|
combo.setBinding("nasal", "gui.weightChangeHandler()");
|
|
} else {
|
|
var slider = tcell(weightTable, "slider", i+1, 1);
|
|
slider.set("property", wprop ~ "/weight-lb");
|
|
var min = w.getNode("min-lb", 1).getValue();
|
|
var max = w.getNode("max-lb", 1).getValue();
|
|
slider.set("min", min != nil ? min : 0);
|
|
slider.set("max", max != nil ? max : 100);
|
|
slider.set("live", 1);
|
|
slider.setBinding("dialog-apply");
|
|
}
|
|
|
|
var lbs = tcell(weightTable, "text", i+1, 2);
|
|
lbs.set("property", wprop ~ "/weight-lb");
|
|
lbs.set("label", "0123456");
|
|
lbs.set("format", "%.0f");
|
|
lbs.set("live", 1);
|
|
}
|
|
|
|
# All done: pop it up
|
|
fgcommand("dialog-new", dialog[name].prop());
|
|
showDialog(name);
|
|
}
|
|
|
|
|
|
|
|
|
|
##
|
|
# Dynamically generates a dialog from a help node.
|
|
#
|
|
# gui.showHelpDialog([<path> [, toggle]])
|
|
#
|
|
# path ... path to help node
|
|
# toggle ... decides if an already open dialog should be closed
|
|
# (useful when calling the dialog from a key binding; default: 0)
|
|
#
|
|
# help node
|
|
# =========
|
|
# each of <title>, <key>, <line>, <text> is optional; uses
|
|
# "/sim/description" or "/sim/aircraft" if <title> is omitted;
|
|
# only the first <text> is displayed
|
|
#
|
|
#
|
|
# <help>
|
|
# <title>dialog title<title>
|
|
# <key>
|
|
# <name>g/G</name>
|
|
# <desc>gear up/down</desc>
|
|
# </key>
|
|
#
|
|
# <line>one line</line>
|
|
# <line>another line</line>
|
|
#
|
|
# <text>text in
|
|
# scrollable widget
|
|
# </text>
|
|
# </help>
|
|
#
|
|
var showHelpDialog = func(path, toggle=0) {
|
|
var node = props.globals.getNode(path);
|
|
if (path == "/sim/help" and size(node.getChildren()) < 4) {
|
|
node = node.getChild("common");
|
|
}
|
|
|
|
var name = node.getNode("title", 1).getValue();
|
|
if (name == nil) {
|
|
name = getprop("/sim/description");
|
|
if (name == nil) {
|
|
name = getprop("/sim/aircraft");
|
|
}
|
|
}
|
|
var toggle = toggle > 0;
|
|
if (toggle and contains(dialog, name)) {
|
|
fgcommand("dialog-close", props.Node.new({ "dialog-name": name }));
|
|
delete(dialog, name);
|
|
return;
|
|
}
|
|
|
|
dialog[name] = Widget.new();
|
|
dialog[name].set("layout", "vbox");
|
|
dialog[name].set("default-padding", 0);
|
|
dialog[name].set("name", name);
|
|
|
|
# title bar
|
|
var titlebar = dialog[name].addChild("group");
|
|
titlebar.set("layout", "hbox");
|
|
titlebar.addChild("empty").set("stretch", 1);
|
|
titlebar.addChild("text").set("label", name);
|
|
titlebar.addChild("empty").set("stretch", 1);
|
|
|
|
var w = titlebar.addChild("button");
|
|
w.set("pref-width", 16);
|
|
w.set("pref-height", 16);
|
|
w.set("legend", "");
|
|
w.set("default", 1);
|
|
w.set("key", "esc");
|
|
w.setBinding("nasal", "delete(gui.dialog, \"" ~ name ~ "\")");
|
|
w.setBinding("dialog-close");
|
|
|
|
dialog[name].addChild("hrule");
|
|
|
|
# key list
|
|
var keylist = dialog[name].addChild("group");
|
|
keylist.set("layout", "table");
|
|
keylist.set("default-padding", 2);
|
|
var keydefs = node.getChildren("key");
|
|
var n = size(keydefs);
|
|
var row = var col = 0;
|
|
foreach (var key; keydefs) {
|
|
if (n >= 60 and row >= n / 3 or n >= 16 and row >= n / 2) {
|
|
col += 1;
|
|
row = 0;
|
|
}
|
|
|
|
var w = keylist.addChild("text");
|
|
w.set("row", row);
|
|
w.set("col", 2 * col);
|
|
w.set("halign", "right");
|
|
w.set("label", " " ~ key.getNode("name").getValue());
|
|
|
|
w = keylist.addChild("text");
|
|
w.set("row", row);
|
|
w.set("col", 2 * col + 1);
|
|
w.set("halign", "left");
|
|
w.set("label", "... " ~ key.getNode("desc").getValue() ~ " ");
|
|
row += 1;
|
|
}
|
|
|
|
# separate lines
|
|
var lines = node.getChildren("line");
|
|
if (size(lines)) {
|
|
if (size(keydefs)) {
|
|
dialog[name].addChild("empty").set("pref-height", 4);
|
|
dialog[name].addChild("hrule");
|
|
dialog[name].addChild("empty").set("pref-height", 4);
|
|
}
|
|
|
|
var g = dialog[name].addChild("group");
|
|
g.set("layout", "vbox");
|
|
g.set("default-padding", 1);
|
|
foreach (var lin; lines) {
|
|
foreach (var l; split("\n", lin.getValue())) {
|
|
var w = g.addChild("text");
|
|
w.set("halign", "left");
|
|
w.set("label", " " ~ l ~ " ");
|
|
}
|
|
}
|
|
}
|
|
|
|
# scrollable text area
|
|
if (node.getNode("text") != nil) {
|
|
dialog[name].set("resizable", 1);
|
|
dialog[name].addChild("empty").set("pref-height", 10);
|
|
|
|
var width = [640, 800, 1152][col];
|
|
var height = screenHProp.getValue() - (100 + (size(keydefs) / (col + 1) + size(lines)) * 28);
|
|
if (height < 200) {
|
|
height = 200;
|
|
}
|
|
|
|
var w = dialog[name].addChild("textbox");
|
|
w.set("padding", 4);
|
|
w.set("halign", "fill");
|
|
w.set("valign", "fill");
|
|
w.set("stretch", "true");
|
|
w.set("slider", 20);
|
|
w.set("pref-width", width);
|
|
w.set("pref-height", height);
|
|
w.set("editable", 0);
|
|
w.set("property", node.getPath() ~ "/text");
|
|
} else {
|
|
dialog[name].addChild("empty").set("pref-height", 8);
|
|
}
|
|
fgcommand("dialog-new", dialog[name].prop());
|
|
showDialog(name);
|
|
}
|
|
|
|
|
|
var debug_keys = {
|
|
title: "Development Keys",
|
|
key: [
|
|
#{ name: "Ctrl-U", desc: "add 1000 ft of emergency altitude" },
|
|
{ name: "Shift-F3", desc: "load panel" },
|
|
{ name: "/", desc: "open property browser" },
|
|
],
|
|
};
|
|
|
|
var basic_keys = {
|
|
title: "Basic Keys",
|
|
key: [
|
|
{ name: "?", desc: "show/hide aircraft help dialog" },
|
|
#{ name: "Tab", desc: "show/hide aircraft config dialog" },
|
|
{ name: "Esc", desc: "quit FlightGear" },
|
|
{ name: "Shift-Esc", desc: "reset FlightGear" },
|
|
{ name: "a/A", desc: "increase/decrease speed-up" },
|
|
{ name: "c", desc: "toggle 3D/2D cockpit" },
|
|
{ name: "Ctrl-C", desc: "toggle clickable panel hotspots" },
|
|
{ name: "p", desc: "pause/continue sim" },
|
|
{ name: "Ctrl-R", desc: "activate instant replay system" },
|
|
{ name: "t/T", desc: "adjust time of day forward/backward" },
|
|
{ name: "v/V", desc: "cycle views (forward/backward)" },
|
|
{ name: "Ctrl-V", desc: "select cockpit view" },
|
|
{ name: "x/X", desc: "zoom in/out" },
|
|
{ name: "Ctrl-X", desc: "reset zoom to default" },
|
|
{ name: "z/Z", desc: "increase/decrease visibility" },
|
|
{ name: "Ctrl-Z", desc: "reset visibility to default" },
|
|
{ name: "'", desc: "display ATC setting dialog" },
|
|
{ name: "+", desc: "let ATC/instructor repeat last message" },
|
|
{ name: "-", desc: "open chat dialog" },
|
|
{ name: "_", desc: "compose chat message" },
|
|
{ name: "F3", desc: "capture screen" },
|
|
{ name: "F10", desc: "toggle menubar" },
|
|
#{ name: "Shift-F1", desc: "load flight" },
|
|
#{ name: "Shift-F2", desc: "save flight" },
|
|
{ name: "Shift-F10", desc: "toggle fullscreen" },
|
|
],
|
|
};
|
|
|
|
var common_aircraft_keys = {
|
|
title: "Common Aircraft Keys",
|
|
key: [
|
|
{ name: "Enter", desc: "move rudder right" },
|
|
{ name: "0/Insert", desc: "move rudder left" },
|
|
{ name: "1/End", desc: "elevator trim up" },
|
|
{ name: "2/Down", desc: "elevator up or increase AP altitude" },
|
|
{ name: "3/PgDn", desc: "decr. throttle or AP autothrottle" },
|
|
{ name: "4/Left", desc: "move aileron left or adj. AP hdg." },
|
|
{ name: "5/KP5", desc: "center aileron, elev., and rudder" },
|
|
{ name: "6/Right", desc: "move aileron right or adj. AP hdg." },
|
|
{ name: "7/Home", desc: "elevator trim down" },
|
|
{ name: "8/Up", desc: "elevator down or decrease AP altitude" },
|
|
{ name: "9/PgUp", desc: "incr. throttle or AP autothrottle" },
|
|
{ name: "Space", desc: "PTT - Push To Talk (via VoIP)" },
|
|
{ name: "!/@/#/$", desc: "select engine 1/2/3/4" },
|
|
{ name: "b", desc: "apply all brakes" },
|
|
{ name: "B", desc: "toggle parking brake" },
|
|
#{ name: "Ctrl-B", desc: "toggle speed brake" },
|
|
{ name: "g/G", desc: "gear up/down" },
|
|
{ name: "h", desc: "cycle HUD (head up display)" },
|
|
{ name: "H", desc: "cycle HUD brightness" },
|
|
#{ name: "i/Shift-i", desc: "normal/alternative HUD" },
|
|
#{ name: "j", desc: "decrease spoilers" },
|
|
#{ name: "k", desc: "increase spoilers" },
|
|
{ name: "l", desc: "toggle tail-wheel lock" },
|
|
{ name: "m/M", desc: "mixture richer/leaner" },
|
|
{ name: "n/N", desc: "propeller finer/coarser" },
|
|
{ name: "P", desc: "toggle 2D panel" },
|
|
{ name: "S", desc: "swap panels" },
|
|
{ name: "s", desc: "fire starter on selected eng." },
|
|
{ name: ", .", desc: "left/right brake (comma, period)" },
|
|
{ name: "~", desc: "select all engines (tilde)" },
|
|
{ name: "[ ]", desc: "flaps up/down" },
|
|
{ name: "{ }", desc: "decr/incr magneto on sel. eng." },
|
|
{ name: "Ctrl-A", desc: "AP: toggle altitude lock" },
|
|
{ name: "Ctrl-G", desc: "AP: toggle glide slope lock" },
|
|
{ name: "Ctrl-H", desc: "AP: toggle heading lock" },
|
|
{ name: "Ctrl-N", desc: "AP: toggle NAV1 lock" },
|
|
{ name: "Ctrl-P", desc: "AP: toggle pitch hold" },
|
|
{ name: "Ctrl-S", desc: "AP: toggle auto-throttle" },
|
|
{ name: "Ctrl-T", desc: "AP: toggle terrain lock" },
|
|
{ name: "Ctrl-W", desc: "AP: toggle wing leveler" },
|
|
{ name: "F6", desc: "AP: toggle heading mode" },
|
|
{ name: "F11", desc: "open autopilot dialog" },
|
|
{ name: "F12", desc: "open radio settings dialog" },
|
|
{ name: "Shift-F5", desc: "scroll 2D panel down" },
|
|
{ name: "Shift-F6", desc: "scroll 2D panel up" },
|
|
{ name: "Shift-F7", desc: "scroll 2D panel left" },
|
|
{ name: "Shift-F8", desc: "scroll 2D panel right" },
|
|
],
|
|
};
|
|
|
|
setlistener("/sim/signals/screenshot", func {
|
|
var path = getprop("/sim/paths/screenshot-last");
|
|
var button = { button: { legend: "Ok", default: 1, binding: { command: "dialog-close" }}};
|
|
var success= getprop("/sim/signals/screenshot");
|
|
if (success) {
|
|
popupTip("Screenshot written to '" ~ path ~ "'", 3);
|
|
} else {
|
|
popupTip("Error writing screenshot '" ~ path ~ "'", 600, button);
|
|
}
|
|
});
|
|
|
|
var terrasync_stalled = 0;
|
|
setlistener("/sim/terrasync/stalled", func {
|
|
var stalled = getprop("/sim/terrasync/stalled");
|
|
if (stalled and !terrasync_stalled)
|
|
{
|
|
var button = { button: { legend: "Ok", default: 1, binding: { command: "dialog-close" }}};
|
|
popupTip("Scenery download stalled. Too many errors reported. See log output.", 600, button);
|
|
}
|
|
terrasync_stalled = stalled;
|
|
});
|
|
|
|
var do_welcome = 1;
|
|
setlistener("/sim/signals/fdm-initialized", func {
|
|
var haveTutorials = size(props.globals.getNode("/sim/tutorials", 1).getChildren("tutorial"));
|
|
gui.menuEnable("tutorial-start", haveTutorials);
|
|
if (do_welcome and haveTutorials)
|
|
settimer(func { setprop("/sim/messages/copilot", "Welcome aboard! Need help? Use 'Help -> Tutorials'.");}, 5.0);
|
|
do_welcome = 0;
|
|
});
|
|
|
|
screenHProp = props.globals.getNode("/sim/startup/ysize");
|
|
|
|
props.globals.getNode("/sim/help/debug", 1).setValues(debug_keys);
|
|
props.globals.getNode("/sim/help/basic", 1).setValues(basic_keys);
|
|
props.globals.getNode("/sim/help/common", 1).setValues(common_aircraft_keys);
|
|
|
|
# enable/disable menu entries
|
|
menuEnable("fuel-and-payload", fdm == "yasim" or fdm == "jsb");
|
|
menuEnable("aircraft-checklists", props.globals.getNode("/sim/checklists") != nil);
|
|
var isAutopilotMenuEnabled = func {
|
|
foreach( var apdp; autopilotDisableProps ) {
|
|
if( props.globals.getNode( apdp ) != nil )
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
menuEnable("autopilot", isAutopilotMenuEnabled() );
|
|
menuEnable("joystick-info", size(props.globals.getNode("/input/joysticks", 1).getChildren("js")));
|
|
menuEnable("rendering-buffers", getprop("/sim/rendering/rembrandt/enabled"));
|
|
menuEnable("rembrandt-buffers-choice", getprop("/sim/rendering/rembrandt/enabled"));
|
|
menuEnable("stereoscopic-options", !getprop("/sim/rendering/rembrandt/enabled"));
|
|
menuEnable("vr-options", !getprop("/sim/rendering/rembrandt/enabled") and getprop("/sim/vr/built"));
|
|
menuEnable("sound-config", getprop("/sim/sound/working"));
|
|
menuEnable("swift_connection", getprop("/sim/swift/available"));
|
|
|
|
# frame-per-second display
|
|
var fps = props.globals.getNode("/sim/rendering/fps-display", 1);
|
|
setlistener(fps, fpsDisplay, 1);
|
|
setlistener("/sim/startup/xsize", func {
|
|
if (fps.getValue()) {
|
|
fpsDisplay(0);
|
|
fpsDisplay(1);
|
|
}
|
|
});
|
|
|
|
# frame-latency display
|
|
var latency = props.globals.getNode("/sim/rendering/frame-latency-display", 1);
|
|
setlistener(latency, latencyDisplay, 1);
|
|
setlistener("/sim/startup/xsize", func {
|
|
if (latency.getValue()) {
|
|
latencyDisplay(0);
|
|
latencyDisplay(1);
|
|
}
|
|
});
|
|
|
|
# only enable precipitation if gui *and* aircraft want it
|
|
var p = "/sim/rendering/precipitation-";
|
|
var precip_gui = getprop(p ~ "gui-enable");
|
|
var precip_ac = getprop(p ~ "aircraft-enable");
|
|
props.globals.getNode(p ~ "enable", 1).setAttribute("userarchive", 0); # TODO remove later
|
|
var set_precip = func setprop(p ~ "enable", precip_gui and precip_ac);
|
|
setlistener(p ~ "gui-enable", func(n) set_precip(precip_gui = n.getValue()),1);
|
|
setlistener(p ~ "aircraft-enable", func(n) set_precip(precip_ac = n.getValue()),1);
|
|
|
|
# the autovisibility feature of the menubar
|
|
# automatically show the menubar if the mouse is at the upper edge of the window
|
|
# the menubar is hidden by mouse mode != 0 and a binding to a LMB click in mode
|
|
# 0 in mice.xml
|
|
var menubarAutoVisibilityListener = nil;
|
|
var menubarAutoVisibilityMouseModeListener = nil;
|
|
var menubarAutoVisibilityEdge = props.globals.initNode( "/sim/menubar/autovisibility/edge-size", 5, "INT" );
|
|
var menubarVisibility = props.globals.initNode( "/sim/menubar/visibility", 0, "BOOL" );
|
|
var currentMenubarVisibility = menubarVisibility.getValue();
|
|
var mouseMode = props.globals.initNode( "/devices/status/mice/mouse/mode", 0, "INT" );
|
|
|
|
setlistener( "/sim/menubar/autovisibility/enabled", func(n) {
|
|
if( n.getValue() and menubarAutoVisibilityListener == nil ) {
|
|
currentMenubarVisibility = menubarVisibility.getValue();
|
|
menubarVisibility.setBoolValue( 0 );
|
|
menubarAutoVisibilityListener = setlistener( "/devices/status/mice/mouse/y", func(n) {
|
|
if( n.getValue() == nil ) return;
|
|
if( mouseMode.getValue() != 0 ) return;
|
|
|
|
if( n.getValue() <= menubarAutoVisibilityEdge.getValue() )
|
|
menubarVisibility.setBoolValue( 1 );
|
|
|
|
}, 1, 0 );
|
|
menubarAutoVisibilityListener = setlistener( mouseMode.getPath(), func(n) {
|
|
if( n.getValue() != 0 ) {
|
|
menubarVisibility.setBoolValue( 0 );
|
|
}
|
|
}, 1, 0 );
|
|
}
|
|
|
|
# do not listen to the mouse position if this feature is enabled
|
|
if( n.getValue() == 0 and menubarAutoVisibilityListener != nil ) {
|
|
removelistener( menubarAutoVisibilityListener );
|
|
removelistener( menubarAutoVisibilityMouseModeListener );
|
|
menubarAutoVisibilityListener = nil;
|
|
menubarAutoVisibilityMouseModeListener = nil;
|
|
menubarVisibility.setBoolValue(currentMenubarVisibility);
|
|
}
|
|
}, 1, 0);
|
|
|
|
setlistener(
|
|
"sim/rendering/composite-viewer-enabled",
|
|
func(node) {
|
|
var cv_enabled = node.getBoolValue();
|
|
menuEnable("view-clone", cv_enabled);
|
|
menuEnable("view-push", cv_enabled);
|
|
menuEnable("view-last-pair", cv_enabled);
|
|
menuEnable("view-last-pair-double", cv_enabled);
|
|
},
|
|
1, # init - trigger immediately.
|
|
0, # type - trigger only when value changed.
|
|
);
|