## # 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 1 # second. 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. :) # popupTip = func { delay = if(size(arg) > 1) {arg[1]} else {DELAY}; tmpl = { name : "PopTip", modal : 0, layout : "hbox", y: screenHProp.getValue() - 140, text : { label : arg[0], padding : 6 } }; popdown(); fgcommand("dialog-new", props.Node.new(tmpl)); fgcommand("dialog-show", tipArg); currTimer = currTimer + 1; thisTimer = currTimer; # Final argument is a flag to use "real" time, not simulated time settimer(func { if(currTimer == thisTimer) { popdown() } }, DELAY, 1); } showDialog = func { fgcommand("dialog-show", props.Node.new({ "dialog-name" : arg[0]})); } ## # Enable/disable named menu entry # menuEnable = func(searchname, state) { foreach (menu; props.globals.getNode("/sim/menubar/default").getChildren("menu")) { foreach (name; menu.getChildren("name")) { if (name.getValue() == searchname) { menu.getNode("enabled").setBoolValue(state); } } foreach (item; menu.getChildren("item")) { foreach (name; item.getChildren("name")) { if (name.getValue() == searchname) { item.getNode("enabled").setBoolValue(state); } } } } } ######################################################################## # 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... # screenHProp = tipArg = nil; INIT = func { screenHProp = props.globals.getNode("/sim/startup/ysize"); tipArg = props.Node.new({ "dialog-name" : "PopTip" }); 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", getprop("/sim/flight-model") == "yasim"); menuEnable("autopilot", props.globals.getNode("/autopilot/KAP140/locks") == nil); menuEnable("tutorial-start", size(props.globals.getNode("/sim/tutorials").getChildren("tutorial"))); menuEnable("joystick-info", size(props.globals.getNode("/input/joysticks").getChildren("js"))); 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) } }); } settimer(INIT, 1); ## # Show/hide the fps display dialog. # fpsDisplay = func { var w = (caller(0)[0]["arg"] == nil) ? cmdarg().getBoolValue() : arg[0]; fgcommand(w ? "dialog-show" : "dialog-close", props.Node.new({"dialog-name": "fps"})); } ## # How many seconds do we show the tip? # DELAY = 1.0; ## # Pop down the tip dialog, if it is visible. # popdown = func { fgcommand("dialog-close", tipArg); } # 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. 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. # Widget = { set : func { me.node.getNode(arg[0], 1).setValue(arg[1]); }, prop : func { return me.node; }, new : func { return { parents : [Widget], node : props.Node.new() } }, addChild : func { type = arg[0]; idx = size(me.node.getChildren(type)); name = type ~ "[" ~ idx ~ "]"; 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(); ... use dialog from $FG_ROOT/gui/dialogs/ # # (A) Dialog.new(, [, ]); # ... load aircraft specific dialog from # under property and under # 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 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(); # Dialog = { new : func(prop, path = nil, name = nil) { var m = { parents : [Dialog] }; m.state = 0; m.name = name; if (path == nil) { # global dialog in $FG_ROOT/gui/dialogs/ m.prop = props.Node.new({ "dialog-name" : prop }); } else { # aircraft dialog with given path 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; }, # 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(); fgcommand("loadxml", props.Node.new({"filename": getprop("/sim/fg-root") ~ "/" ~ me.path, "targetnode": me.prop.getPath()})); var n = me.prop.getNode("name"); if (n == nil) die("Dialog class: XML dialog must have "); 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; }, instance : {}, }; ## # FileSelector class (derived from Dialog class). # # SYNOPSIS: FileSelector.new( [, [, [, [, ]]]) # # callback ... callback function that gets return value as cmdarg().getValue() # oper ... string that describes purpose (put on the "OK" button) # dir ... starting dir ($FG_ROOT if unset) # file ... pre-selected default file name # hidden ... flag that decids whether UNIX dotfiles should be shown (1) or not (0) # # EXAMPLE: # # var report = func { print("file ", cmdarg().getValue(), " selected") } # var selector = gui.FileSelector.new(report, "Save Flight", "/tmp", "flight.sav"); # selector.open(); # see the Dialog class for other methods # var FileSelector = { new : func(callback, operation = "OK", dir = "", file = "", show_hidden = 0) { var name = "file-select-" ~ int(1e9 * rand()); var data = props.globals.getNode("/sim/gui/dialogs/" ~ name, 1); var m = Dialog.new(data.getNode("dialog", 1), "gui/dialogs/file-select.xml", name); m.parents = [FileSelector, Dialog]; data.getNode("operation", 1).setValue(operation); data.getNode("directory", 1).setValue(dir); data.getNode("selection", 1).setValue(file); data.getNode("show-hidden", 1).setBoolValue(show_hidden); m.cblistener = setlistener(data.getNode("path", 1), callback); return m; }, del : func { me.close(); delete(me.instance, me.name); removelistener(me.cblistener); me.prop.getParent().removeChild(me.prop.getName(), me.prop.getIndex()); }, }; ## # Open property browser with given target path. # var property_browser = func(dir = "/") { var dlgname = "property-browser"; foreach (var module; keys(globals)) { if (find("__dlg:" ~ dlgname, module) == 0) { globals[module].clone(dir); return; } } 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")) { var path = b.getValue(); if (path != nil and size(path)) property_browser(path); } props.globals.removeChildren("browser"); }, 0); ## # Apply whole dialog or list of widgets. This copies the widgets' # visible contents to the respective . # 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 . # 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); } } ######################################################################## # GUI theming ######################################################################## nextStyle = func { numStyles = size(props.globals.getNode("/sim/gui").getChildren("style")); curr = getprop("/sim/gui/current-style") + 1; if (curr >= numStyles) { curr = 0; } setprop("/sim/gui/current-style", curr); fgcommand("gui-redraw"); } ######################################################################## # Dialog Boxes ######################################################################## 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)... if(wgt.getNode("tank") == nil) { return 0; } var ti = wgt.getNode("tank").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; foreach(w; props.globals.getNode("sim").getChildren("weight")) { var selected = w.getNode("selected"); if(selected != nil) { foreach(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/reset", func { cmdarg().getBoolValue() 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. if(tankchanged) { var p = props.Node.new({"dialog-name" : "WeightAndFuel"}); fgcommand("dialog-close", p); showWeightDialog(); } } ## # Dynamically generates a weight & fuel configuration dialog specific to # the aircraft. # showWeightDialog = func { name = "WeightAndFuel"; title = "Weight and Fuel Settings"; # # General Dialog Structure # dialog[name] = Widget.new(); dialog[name].set("name", name); dialog[name].set("layout", "vbox"); header = dialog[name].addChild("text"); header.set("label", title); dialog[name].addChild("hrule"); if (props.globals.getNode("/yasim") == nil) { msg = dialog[name].addChild("text"); msg.set("label", "Not supported for this aircraft"); cancel = dialog[name].addChild("button"); cancel.set("legend", "Cancel"); cancel.setBinding("dialog-close"); fgcommand("dialog-new", dialog[name].prop()); showDialog(name); return; } contentArea = dialog[name].addChild("group"); contentArea.set("layout", "hbox"); grossWgt = props.globals.getNode("/yasim/gross-weight-lbs"); if(grossWgt != nil) { gwg = dialog[name].addChild("group"); gwg.set("layout", "hbox"); gwg.addChild("empty").set("stretch", 1); gwg.addChild("text").set("label", "Gross Weight:"); txt = gwg.addChild("text"); txt.set("label", "0123456789"); txt.set("format", "%.0f lb"); txt.set("property", "/yasim/gross-weight-lbs"); txt.set("live", 1); gwg.addChild("empty").set("stretch", 1); } buttonBar = dialog[name].addChild("group"); buttonBar.set("layout", "hbox"); buttonBar.set("default-padding", 10); ok = buttonBar.addChild("button"); ok.set("legend", "OK"); ok.set("key", "esc"); ok.setBinding("dialog-apply"); ok.setBinding("dialog-close"); # Temporary helper function tcell = func(parent, type, row, col) { cell = parent.addChild(type); cell.set("row", row); cell.set("col", col); return cell; } # # Fill in the content area # fuelArea = contentArea.addChild("group"); fuelArea.set("layout", "vbox"); fuelArea.addChild("text").set("label", "Fuel Tanks"); 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"); tanks = props.globals.getNode("/consumables/fuel").getChildren("tank"); for(i=0; i [, 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 , <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> # showHelpDialog = func { node = props.globals.getNode(arg[0]); if (arg[0] == "/sim/help" and size(node.getChildren()) < 4) { node = node.getChild("common"); } name = node.getNode("title", 1).getValue(); if (name == nil) { name = getprop("/sim/description"); if (name == nil) { name = getprop("/sim/aircraft"); } } toggle = size(arg) > 1 and arg[1] != nil and arg[1] > 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 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); 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 keylist = dialog[name].addChild("group"); keylist.set("layout", "table"); keylist.set("default-padding", 2); keydefs = node.getChildren("key"); n = size(keydefs); row = col = 0; foreach (key; keydefs) { if (n >= 60 and row >= n / 3 or n >= 16 and row >= n / 2) { col += 1; row = 0; } 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 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); } 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())) { w = g.addChild("text"); w.set("halign", "left"); w.set("label", " " ~ l ~ " "); } } } # scrollable text area if (node.getNode("text") != nil) { dialog[name].addChild("empty").set("pref-height", 10); width = [640, 800, 1152][col]; height = screenHProp.getValue() - (100 + (size(keydefs) / (col + 1) + size(lines)) * 28); if (height < 200) { height = 200; } w = dialog[name].addChild("textbox"); w.set("halign", "fill"); 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); } debug_keys = { title : "Development Keys", key : [ #{ name : "Ctrl-U", desc : "add 1000 ft of emergency altitude" }, { name : "F2", desc : "force tile cache reload" }, { name : "F4", desc : "force lighting update" }, { name : "F8", desc : "cycle fog type" }, { name : "F9", desc : "toggle textures" }, { name : "Shift-F3", desc : "load panel" }, { name : "Shift-F4", desc : "reload global preferences" }, { name : "Shift-F9", desc : "toggle FDM data logging" }, { name : "Shift-Space", desc : "open property browser" }, ], }; 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 : "r", desc : "activate instant replay system" }, { name : "Ctrl-R", desc : "show radio setting dialog" }, { name : "t/T", desc : "increase/decrease warp delta" }, { name : "v/V", desc : "cycle views (forward/backward)" }, { name : "Ctrl-V", desc : "select cockpit view" }, { name : "w/W", desc : "increase/decrease warp" }, { name : "x/X", desc : "zoom in/out" }, { name : "Ctrl-X", desc : "reset zoom to default" }, { name : "z/Z", desc : "increase/decrease visibility" }, { name : "'", desc : "display ATC setting dialog" }, { name : "+", desc : "let ATC/instructor repeat last message" }, { name : "F1", desc : "load flight" }, { name : "F3", desc : "capture screen" }, { name : "F10", desc : "toggle menubar" }, { name : "Shift-F2", desc : "save flight" }, { name : "Shift-F10", desc : "cycle through GUI styles" }, ], }; 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 : "decrease elevator trim" }, { name : "2/Up", desc : "increase elevator or 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 : "increase elevator trim" }, { name : "8/Down", desc : "decrease elevator or AP altitude" }, { name : "9/PgUp", desc : "incr. throttle or AP autothrottle" }, { name : "Space", desc : "fire starter on selected eng." }, { 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/minimal 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 : ", .", 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 : "pop up autopilot (AP) 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" }, ], };