diff --git a/Nasal/IOrules b/Nasal/IOrules
index b130958f7..a67bcf3da 100644
--- a/Nasal/IOrules
+++ b/Nasal/IOrules
@@ -38,3 +38,5 @@ WRITE ALLOW $FG_HOME/state/*.xml
WRITE ALLOW $FG_HOME/aircraft-data/*.xml
WRITE ALLOW $FG_HOME/Wildfire/*.xml
WRITE ALLOW $FG_HOME/runtime-jetways/*.xml
+WRITE ALLOW $FG_HOME/Input/Joysticks/*.xml
+
diff --git a/Nasal/joystick.nas b/Nasal/joystick.nas
new file mode 100644
index 000000000..f79481c14
--- /dev/null
+++ b/Nasal/joystick.nas
@@ -0,0 +1,686 @@
+# Joystick configuration library.
+var DIALOGROOT = "/sim/gui/dialogs/joystick-config";
+var MAX_AXES = 8;
+var MAX_BUTTONS = 24;
+
+# Hash of the custom axis/buttons
+var custom_bindings = {};
+
+# Class for an individual joystick axis binding
+var Axis = {
+ new: func(name, prop, invertable) {
+ var m = { parents: [Axis] };
+ m.name = name;
+ m.prop = prop;
+ m.invertable = invertable;
+ m.inverted = 0;
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [Axis] };
+ m.name = me.name;
+ m.prop = me.prop;
+ m.invertable = me.invertable;
+ m.inverted = me.inverted;
+ return m;
+ },
+
+ match: func(prop) {
+ return 0;
+ },
+
+ parse: func(prop) { },
+
+ readProps: func(props) {},
+
+ getName: func() { return me.name; },
+ getBinding: func(axis) { return props.Node.new(); },
+ isInvertable: func() { return me.invertable; },
+ isInverted: func() { return me.inverted; },
+
+ setInverted: func(b) {
+ if (me.invertable) me.inverted = b;
+ },
+};
+
+var CustomAxis = {
+ new: func() {
+ var m = { parents: [CustomAxis, Axis.new("Custom", "", 0) ] };
+ me.custom_binding = nil;
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [CustomAxis, Axis.new("Custom", "", 0) ] };
+ m.custom_binding = me.custom_binding;
+ return m;
+ },
+
+ match: func(prop) {
+ var p = props.Node.new();
+
+ if (prop.getNode("binding") != nil) {
+ props.copy(prop.getNode("binding"), p);
+ me.custom_binding = p;
+ return 1;
+ } else {
+ return 0;
+ }
+ },
+
+ getBinding: func(axis) {
+ var p = props.Node.new().getNode("axis[" ~ axis ~ "]", 1);
+ p.getNode("desc", 1).setValue(me.name);
+ props.copy(me.custom_binding, p.getNode("binding", 1));
+ return p;
+ },
+};
+
+var UnboundAxis = {
+ new: func() {
+ var m = { parents: [UnboundAxis, Axis.new("None", "", 0) ] };
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [UnboundAxis, Axis.new("None", "", 0) ] };
+ return m;
+ },
+
+ match: func(prop) {
+ return 1;
+ },
+
+ getBinding: func(axis) {
+ return nil;
+ },
+};
+
+
+var PropertyScaleAxis = {
+ new: func(name, prop, deadband=0, factor=1, offset=0) {
+ var m = { parents: [PropertyScaleAxis, Axis.new(name, prop, 1) ] };
+ m.prop=prop;
+ m.deadband = deadband;
+ m.factor = factor;
+ m.offset = offset;
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [PropertyScaleAxis, Axis.new(me.name, me.prop, 1) ] };
+
+ m.inverted = me.inverted;
+
+ m.prop= me.prop;
+ m.deadband = me.deadband;
+ m.factor = me.factor;
+ m.offset = me.offset;
+ return m;
+ },
+
+
+ match: func(prop) {
+ var cmd = prop.getNode("binding", 1).getNode("command", 1).getValue();
+ var p = prop.getNode("binding", 1).getNode("property", 1).getValue();
+ return ((cmd == "property-scale") and (p == me.prop));
+ },
+
+ parse: func(p) {
+ me.deadband = p.getNode("binding", 1).getNode("dead-band", 1).getValue();
+ if (p.getNode("binding", 1).getNode("factor", 1).getValue() != nil) {
+ me.inverted = (p.getNode("binding", 1).getNode("factor", 1).getValue() < 0);
+ }
+ me.offset = p.getNode("binding", 1).getNode("offset", 1).getValue();
+ },
+
+ getBinding: func(axis) {
+ var p = props.Node.new();
+ p = p.getNode("axis[" ~ axis ~ "]", 1);
+ p.getNode("desc", 1).setValue(me.name);
+ p.getNode("binding", 1).getNode("command", 1).setValue("property-scale");
+ p.getNode("binding", 1).getNode("property", 1).setValue(me.prop);
+ p.getNode("binding", 1).getNode("dead-band", 1).setValue(me.deadband);
+ if (me.inverted) {
+ p.getNode("binding", 1).getNode("factor", 1).setValue(0 - me.factor);
+ } else {
+ p.getNode("binding", 1).getNode("factor", 1).setValue(me.factor);
+ }
+ p.getNode("binding", 1).getNode("offset", 1).setValue(me.offset);
+ return p;
+ },
+
+};
+
+var NasalScaleAxis = {
+ new: func(name, script, prop) {
+ var m = { parents: [NasalScaleAxis, Axis.new(name, prop, 0) ] };
+ m.script = script;
+ m.prop = prop;
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [NasalScaleAxis, Axis.new(me.name, me.prop, 0) ] };
+
+ m.script = me.script;
+ m.prop = me.prop;
+ return m;
+ },
+
+ match: func(prop) {
+ var cmd = prop.getNode("binding", 1).getNode("command", 1).getValue();
+ var p = prop.getNode("binding", 1).getNode("script", 1).getValue();
+ if ((p != nil) and (cmd == "nasal")) {
+ p = string.trim(p);
+ p = string.replace(p, ";", "");
+ p = p ~ ";";
+ return (p == me.script);
+ } else {
+ return 0;
+ }
+ },
+
+ getBinding: func(axis) {
+ var p = props.Node.new().getNode("axis[" ~ axis ~ "]", 1);
+ p.getNode("desc", 1).setValue(me.name);
+ p.getNode("binding", 1).getNode("command", 1).setValue("nasal");
+ p.getNode("binding", 1).getNode("script", 1).setValue(me.script);
+ return p;
+ },
+};
+
+var NasalLowHighAxis = {
+ new: func(name, lowscript, highscript, prop) {
+ var m = { parents: [NasalLowHighAxis, Axis.new(name, prop, 1) ] };
+ m.lowscript = lowscript;
+ m.highscript = highscript;
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [NasalLowHighAxis, Axis.new(me.name, me.prop, 1) ] };
+
+ m.inverted = me.inverted;
+
+ m.lowscript = me.lowscript;
+ m.highscript = me.highscript;
+ return m;
+ },
+
+ match: func(prop) {
+ var cmd = prop.getNode("low", 1).getNode("binding", 1).getNode("command", 1).getValue();
+ var p = prop.getNode("low", 1).getNode("binding", 1).getNode("script", 1).getValue();
+
+ if ((p == nil) or (cmd != "nasal")) return 0;
+ p = string.trim(p);
+ p = string.replace(p, ";", "");
+ p = p ~ ";";
+
+ if (p == me.lowscript) {
+ return 1;
+ }
+
+ if (p == me.highscript) {
+ me.inverted = 1;
+ return 1;
+ }
+
+ return 0;
+ },
+
+ getBinding: func(axis) {
+ var p = props.Node.new().getNode("axis[" ~ axis ~ "]", 1);
+ p.getNode("desc", 1).setValue(me.name);
+
+ p.getNode("low", 1).getNode("binding", 1).getNode("command", 1).setValue("nasal");
+ p.getNode("high", 1).getNode("binding", 1).getNode("command", 1).setValue("nasal");
+
+ if (me.inverted) {
+ p.getNode("low", 1).getNode("binding", 1).getNode("script", 1).setValue(me.highscript);
+ p.getNode("high", 1).getNode("binding", 1).getNode("script", 1).setValue(me.lowscript);
+ } else {
+ p.getNode("low", 1).getNode("binding", 1).getNode("script", 1).setValue(me.lowscript);
+ p.getNode("high", 1).getNode("binding", 1).getNode("script", 1).setValue(me.highscript);
+ }
+
+ return p;
+ },
+};
+
+
+var axisBindings = [
+ PropertyScaleAxis.new("Aileron", "/controls/flight/aileron"),
+ PropertyScaleAxis.new("Elevator", "/controls/flight/elevator"),
+ PropertyScaleAxis.new("Rudder", "/controls/flight/rudder"),
+ NasalScaleAxis.new("Throttle", "controls.throttleAxis();", "/controls/engines/engine[0]/throttle") ,
+ NasalScaleAxis.new("Mixture", "controls.mixtureAxis();", "/controls/engines/engine[0]/mixture") ,
+ NasalScaleAxis.new("Propeller", "controls.propellerAxis();", "/controls/engines/engine[0]/propeller-pitch") ,
+ NasalLowHighAxis.new("View (horizontal)",
+ "setprop(\"/sim/current-view/goal-heading-offset-deg\", getprop(\"/sim/current-view/goal-heading-offset-deg\") + 30);",
+ "setprop(\"/sim/current-view/goal-heading-offset-deg\", getprop(\"/sim/current-view/goal-heading-offset-deg\") - 30);",
+ "/sim/current-view/goal-heading-offset-deg"),
+ NasalLowHighAxis.new("View (vertical)",
+ "setprop(\"/sim/current-view/goal-pitch-offset-deg\", getprop(\"/sim/current-view/goal-pitch-offset-deg\") - 20);",
+ "setprop(\"/sim/current-view/goal-pitch-offset-deg\", getprop(\"/sim/current-view/goal-pitch-offset-deg\") + 20);",
+ "/sim/current-view/goal-heading-offset-deg"),
+# PropertyScaleAxis.new("Aileron Trim", "/controls/flight/aileron-trim"),
+# PropertyScaleAxis.new("Elevator Trim", "/controls/flight/elevator-trim"),
+# PropertyScaleAxis.new("Rudder Trim", "/controls/flight/rudder-trim"),
+ PropertyScaleAxis.new("Brake Left", "/controls/gear/brake-left", 0, 0.5, 1.0),
+ PropertyScaleAxis.new("Brake Right", "/controls/gear/brake-right", 0, 0.5, 1.0),
+ NasalLowHighAxis.new("Aileron Trim", "controls.aileronTrim(-1);", "controls.aileronTrim(1);", "/controls/flight/aileron-trim"),
+ NasalLowHighAxis.new("Elevator Trim", "controls.elevatorTrim(-1);", "controls.elevatorTrim(1);", "/controls/flight/elevator-trim"),
+ NasalLowHighAxis.new("Rudder Trim", "controls.rudderTrim(-1);", "controls.rudderTrim(1);", "/controls/flight/rudder-trim"),
+ CustomAxis.new(),
+ UnboundAxis.new(),
+];
+
+# Button bindings
+var ButtonBinding = {
+ new: func(name, binding, repeatable) {
+ var m = { parents: [ButtonBinding] };
+ m.name = name;
+ m.binding = binding;
+ m.repeatable = repeatable;
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [ButtonBinding] };
+ m.name = me.name;
+ m.binding= me.binding;
+ m.repeatable = me.repeatable;
+ return m;
+ },
+
+ match: func(prop) {
+ return 0;
+ },
+
+ getName: func() { return me.name; },
+ getBinding: func(button) { return nil; },
+ isRepeatable: func() { return me.repeatable; }
+};
+
+var CustomButton = {
+ new: func() {
+ var m = { parents: [CustomButton, ButtonBinding.new("Custom", "", 0) ] };
+ m.custom_binding = nil;
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [CustomButton, ButtonBinding.new("Custom", "", 0) ] };
+ m.custom_binding = me.custom_binding;
+ return m;
+ },
+
+ match: func(prop) {
+ if (prop.getNode("binding") != nil) {
+ var p = props.Node.new();
+ props.copy(prop.getNode("binding"), p);
+ me.custom_binding = p;
+ return 1;
+ } else {
+ return 0;
+ }
+ },
+
+ getBinding: func(button) {
+ var p = props.Node.new().getNode("button[" ~ button ~ "]", 1);
+ p.getNode("desc", 1).setValue(me.name);
+ props.copy(me.custom_binding, p.getNode("binding", 1));
+ return p;
+ },
+};
+
+var UnboundButton = {
+ new: func() {
+ var m = { parents: [UnboundButton, ButtonBinding.new("None", "", 0) ] };
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [UnboundButton, ButtonBinding.new("None", "", 0) ] };
+ return m;
+ },
+
+ match: func(prop) {
+ return (prop.getNode("binding") != nil);
+ },
+
+ getBinding: func(button) {
+ var p = props.Node.new().getNode("button[" ~ button ~ "]", 1);
+ p.getNode("desc", 1).setValue(me.name);
+ return p;
+ },
+};
+
+
+var PropertyToggleButton = {
+ new: func(name, prop) {
+ var m = { parents: [PropertyToggleButton, ButtonBinding.new(name, prop, 0) ] };
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [PropertyToggleButton, ButtonBinding.new(me.name, me.binding, 0) ] };
+ return m;
+ },
+
+ match: func(prop) {
+
+ var c = prop.getNode("binding", 1).getNode("command", 1).getValue();
+ var p = prop.getNode("binding", 1).getNode("property", 1).getValue();
+ return ((c == "property-toggle") and (p == me.prop));
+ },
+
+ getBinding: func(button) {
+ var p = props.Node.new().getNode("button[" ~ button ~ "]", 1);
+ p.getNode("desc", 1).setValue(me.name);
+ p.getNode("binding", 1).getNode("command", 1).setValue("property-toggle");
+ p.getNode("binding", 1).getNode("property", 1).setValue(me.binding);
+ return p;
+ },
+};
+
+var PropertyAdjustButton = {
+ new: func(name, prop, step) {
+ var m = { parents: [PropertyAdjustButton, ButtonBinding.new(name, prop, 0) ] };
+ m.step = step;
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [PropertyAdjustButton, ButtonBinding.new(me.name, me.binding, 0) ] };
+ m.step = me.step;
+ return m;
+ },
+
+ match: func(prop) {
+ var c = prop.getNode("binding", 1).getNode("command", 1).getValue();
+ var p = prop.getNode("binding", 1).getNode("property", 1).getValue();
+ var s = prop.getNode("binding", 1).getNode("step", 1).getValue();
+ return ((c == "property-adjust") and (p == me.binding) and (s == me.step));
+ },
+
+ getBinding: func(button) {
+ var p = props.Node.new().getNode("button[" ~ button ~ "]", 1);
+ p.getNode("desc", 1).setValue(me.name);
+ p.getNode("binding", 1).getNode("command", 1).setValue("property-adjust");
+ p.getNode("binding", 1).getNode("property", 1).setValue(me.binding);
+ p.getNode("binding", 1).getNode("step", 1).setValue(me.step);
+ return p;
+ },
+};
+
+var NasalButton = {
+ new: func(name, script, repeatable) {
+ var m = { parents: [NasalButton, ButtonBinding.new(name, script, repeatable) ] };
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [NasalButton, ButtonBinding.new(me.name, me.binding, me.repeatable) ] };
+ return m;
+ },
+
+ match: func(prop) {
+ var c = prop.getNode("binding", 1).getNode("command", 1).getValue();
+ var p = prop.getNode("binding", 1).getNode("script", 1).getValue();
+
+ if (p == nil) { return 0; }
+
+ p = string.trim(p);
+ p = string.replace(p, ";", "");
+ p = p ~ ";";
+
+ return ((c == "nasal") and (p == me.binding));
+ },
+
+ getBinding: func(button) {
+ var p = props.Node.new().getNode("button[" ~ button ~ "]", 1);
+ p.getNode("desc", 1).setValue(me.name);
+ p.getNode("binding", 1).getNode("command", 1).setValue("nasal");
+ p.getNode("binding", 1).getNode("script", 1).setValue(me.binding);
+ p.getNode("repeatable", 1).setValue(me.repeatable);
+ return p;
+ },
+};
+
+
+var NasalHoldButton = {
+ new: func(name, script, scriptUp) {
+ var m = { parents: [NasalHoldButton, ButtonBinding.new(name, script, 0) ] };
+ m.scriptUp = scriptUp;
+ return m;
+ },
+
+ clone: func() {
+ var m = { parents: [NasalHoldButton, ButtonBinding.new(me.name, me.binding, 0) ] };
+ m.scriptUp = me.scriptUp;
+ return m;
+ },
+
+ match: func(prop) {
+
+ var c = prop.getNode("mod-up", 1).getNode("binding", 1).getNode("command", 1).getValue();
+ var p1 = prop.getNode("binding", 1).getNode("script", 1).getValue();
+ var p2 = prop.getNode("mod-up", 1).getNode("binding", 1).getNode("script", 1).getValue();
+
+ if (p2 == nil) { return 0; }
+
+ p1 = string.trim(p1);
+ p1 = string.replace(p1, ";", "");
+ p1 = p1 ~ ";";
+
+ return ((c == "nasal") and (p1 == me.binding));
+ },
+
+ getBinding: func(button) {
+ var p = props.Node.new().getNode("button[" ~ button ~ "]", 1);
+ p.getNode("desc", 1).setValue(me.name);
+ p.getNode("repeatable", 1).setValue("false");
+ p.getNode("binding", 1).getNode("command", 1).setValue("nasal");
+ p.getNode("binding", 1).getNode("script", 1).setValue(me.binding);
+ p.getNode("mod-up", 1).getNode("binding", 1).getNode("command", 1).setValue("nasal");
+ p.getNode("mod-up", 1).getNode("binding", 1).getNode("script", 1).setValue(me.scriptUp);
+ return p;
+ },
+};
+
+var buttonBindings = [
+ NasalButton.new("Elevator Trim Up", "controls.elevatorTrim(-1);", 1),
+ NasalButton.new("Elevator Trim Down", "controls.elevatorTrim(1);", 1),
+ NasalButton.new("Rudder Trim Left", "controls.rudderTrim(-1);", 1),
+ NasalButton.new("Rudder Trim Right", "controls.rudderTrim(1);", 1),
+ NasalButton.new("Aileron Trim Left", "controls.aileronTrim(-1);", 1),
+ NasalButton.new("Aileron Trim Right", "controls.aileronTrim(1);", 1),
+ NasalHoldButton.new("FGCom PTT", "controls.ptt(1);", "controls.ptt(-1);"),
+ NasalHoldButton.new("Trigger", "controls.trigger(1);", "controls.trigger(0);"),
+ NasalHoldButton.new("Flaps Up", "controls.flapsDown(-1);", "controls.flapsDown(0);"),
+ NasalHoldButton.new("Flaps Down", "controls.flapsDown(1);", "controls.flapsDown(0);"),
+ NasalHoldButton.new("Gear Up", "controls.gearDown(-1);", "controls.gearDown(0);"),
+ NasalHoldButton.new("Gear Down", "controls.gearDown(1);", "controls.gearDown(0);"),
+ NasalHoldButton.new("Spoilers Retract", "controls.stepSpoilers(-1);", "controls.stepSpoilers(0);"),
+ NasalHoldButton.new("Spoilers Deploy", "controls.stepSpoilers(1);", "controls.stepSpoilers(0);"),
+ NasalHoldButton.new("Brakes", "controls.applyBrakes(1);", "controls.applyBrakes(0);"),
+
+ NasalButton.new("View Decrease", "view.decrease(0.75);", 1),
+ NasalButton.new("View Increase", "view.increase(0.75);", 1),
+ NasalButton.new("View Cycle Forwards", "view.stepView(1);", 0),
+ NasalButton.new("View Cycle Backwards", "view.stepView(-1);", 0),
+ PropertyAdjustButton.new("View Left", "/sim/current-view/goal-heading-offset-deg", "30.0"),
+ PropertyAdjustButton.new("View Right", "/sim/current-view/goal-heading-offset-deg", "-30.0"),
+ PropertyAdjustButton.new("View Up", "/sim/current-view/goal-pitch-offset-deg", "20.0"),
+ PropertyAdjustButton.new("View Down", "/sim/current-view/goal-pitch-offset-deg", "-20.0"),
+ CustomButton.new(),
+];
+
+# Parse config from the /input tree and write it to the
+# dialog_root.
+var readConfig = func(dialog_root="/sim/gui/dialogs/joystick-config") {
+
+ var js_name = getprop(dialog_root ~ "/selected-joystick");
+ var joysticks = props.globals.getNode("/input/joysticks").getChildren("js");
+
+ if (size(joystick) == 0) { return 0; }
+
+ if (js_name == nil) {
+ js_name = joysticks[0].getNode("id").getValue();
+ }
+
+ var js = nil;
+
+ forindex (var i; joysticks) {
+ if ((joysticks[i].getNode("id") != nil) and
+ (joysticks[i].getNode("id").getValue() == js_name))
+ {
+ js = joysticks[i];
+ setprop(dialog_root ~ "/selected-joystick", js_name);
+ setprop(dialog_root ~ "/selected-joystick-index", i);
+ setprop(dialog_root ~ "/selected-joystick-config", joysticks[i].getNode("source").getValue());
+ }
+ }
+
+ # Set up the axes assignments
+ var axes = js.getChildren("axis");
+
+ for (var axis = 0; axis < MAX_AXES; axis = axis +1) {
+
+ var p = props.globals.getNode(dialog_root ~ "/axis[" ~ axis ~ "]", 1);
+ p.remove();
+ p = props.globals.getNode(dialog_root ~ "/axis[" ~ axis ~ "]", 1);
+
+ # Note that we can't simply use an index into the axes array
+ # as that doesn't work for a sparsley populated set of axes.
+ # E.g. one with n="3"
+ var a = js.getNode("axis[" ~ axis ~ "]");
+
+ if (a != nil) {
+ # Read properties from bindings
+ props.copy(a, p.getNode("original_binding", 1));
+
+ var binding = nil;
+ foreach (var b; joystick.axisBindings) {
+ if ((binding == nil) and (a != nil) and b.match(a)) {
+ binding = b.clone();
+ binding.parse(a);
+ p.getNode("binding", 1).setValue(binding.getName());
+ p.getNode("invertable", 1).setValue(binding.isInvertable());
+ p.getNode("inverted", 1).setValue(binding.isInverted());
+ }
+ }
+
+ if (binding == nil) {
+ # No binding for this axis
+ p.getNode("binding", 1).setValue("None");
+ p.getNode("invertable", 1).setValue(0);
+ p.getNode("inverted", 1).setValue(0);
+ p.removeChild("original_binding");
+ }
+ } else {
+ p.getNode("binding", 1).setValue("None");
+ p.getNode("invertable", 1).setValue(0);
+ p.getNode("inverted", 1).setValue(0);
+ p.removeChild("original_binding");
+ }
+ }
+
+ # Set up button assignment.
+ var buttons = js.getChildren("button");
+
+ for (var button = 0; button < MAX_BUTTONS; button = button + 1) {
+ var btn = props.globals.getNode(dialog_root ~ "/button[" ~ button ~ "]", 1);
+ btn.remove();
+ btn = props.globals.getNode(dialog_root ~ "/button[" ~ button ~ "]", 1);
+
+ # Note that we can't simply use an index into the buttons array
+ # as that doesn't work for a sparsley populated set of buttons.
+ # E.g. one with n="3"
+ var a = js.getNode("button[" ~ button ~ "]");
+
+ if (a != nil) {
+ # Read properties from bindings
+ props.copy(a, p.getNode("original_binding", 1));
+ var binding = nil;
+ foreach (var b; joystick.buttonBindings) {
+ if ((binding == nil) and (a != nil) and b.match(a)) {
+ binding = b.clone();
+ btn.getNode("binding", 1).setValue(binding.getName());
+ props.copy(b.getBinding(button), btn.getNode("original_binding", 1));
+ }
+ }
+
+ if (b == nil) {
+ btn.getNode("binding", 1).setValue("None");
+ btn.removeChild("original_binding");
+ }
+ } else {
+ btn.getNode("binding", 1).setValue("None");
+ btn.removeChild("original_binding");
+ }
+ }
+}
+
+var writeConfig = func(dialog_root="/sim/gui/dialogs/joystick-config") {
+
+ # Write out the joystick file.
+ var config = props.Node.new();
+ var id = getprop(dialog_root ~ "/selected-joystick");
+ config.getNode("name", 1).setValue(id);
+
+ var axes = props.globals.getNode(dialog_root).getChildren("axis");
+ forindex (var axis; axes) {
+ var name = getprop(dialog_root ~ "/axis[" ~ axis ~ "]/binding");
+
+ if (name != "None") {
+ foreach (var binding; axisBindings) {
+ if (binding.getName() == name) {
+ var b = binding.clone();
+ b.setInverted(getprop(dialog_root ~ "/axis[" ~ axis ~ "]/inverted"));
+
+ # Generate the axis and binding
+ var axisnode = config.getNode("axis[" ~ axis ~ "]", 1);
+ if (name == "Custom") {
+ props.copy(props.globals.getNode(dialog_root ~ "/axis[" ~ axis ~ "]/original_binding", 1), axisnode);
+ } else {
+ props.copy(b.getBinding(axis), axisnode);
+ }
+ }
+ }
+ }
+ }
+
+ var buttons = props.globals.getNode(dialog_root).getChildren("button");
+ forindex (var btn; buttons) {
+ var name = getprop(dialog_root ~ "/button[" ~ btn ~ "]/binding");
+
+ if (name != "None") {
+ foreach (var binding; buttonBindings) {
+ if (binding.getName() == name) {
+ var b = binding.clone();
+ # Generate the axis and binding
+ var buttonprop = config.getNode("button[" ~ btn ~ "]", 1);
+ if (name == "Custom") {
+ props.copy(props.globals.getNode(dialog_root ~ "/button[" ~ btn ~ "]/original_binding", 1), buttonprop);
+ } else {
+ props.copy(b.getBinding(btn), buttonprop);
+ }
+ }
+ }
+ }
+ }
+
+ var filename = id;
+ filename = string.replace(filename, " ", "");
+ filename = string.replace(filename, ".", "");
+ filename = string.replace(filename, "/", "");
+
+ # Write out the file
+ io.write_properties(getprop("/sim/fg-home") ~ "/Input/Joysticks/" ~ filename ~ ".xml", config);
+}
+
+
diff --git a/gui/dialogs/button-config.xml b/gui/dialogs/button-config.xml
new file mode 100644
index 000000000..81d6fb19f
--- /dev/null
+++ b/gui/dialogs/button-config.xml
@@ -0,0 +1,437 @@
+
+
+
+
+
+
+
+
+
+
+ button-config
+ vbox
+ true
+ true
+ 3
+
+
+ hbox
+ 1
+
+ true
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+ left
+
+
+
+
+
+
+ table
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+ hbox
+
+
+
+ true
+
+
+
+
diff --git a/gui/dialogs/joystick-config.xml b/gui/dialogs/joystick-config.xml
new file mode 100644
index 000000000..912298a4e
--- /dev/null
+++ b/gui/dialogs/joystick-config.xml
@@ -0,0 +1,463 @@
+
+
+
+
+ 5) {
+ row = row +1;
+ col = 0;
+ }
+}
+
+var updateConfig = func() {
+ joystick.writeConfig();
+ fgcommand("reinit", props.Node.new({"subsystem": "input"}));
+ fgcommand("dialog-close", props.Node.new({"dialog-name": "joystick-config"}));
+ fgcommand("dialog-show", props.Node.new({"dialog-name": "joystick-config"}));
+}
+ ]]>
+
+
+
+
+
+ joystick-config
+ vbox
+ true
+ 3
+
+
+ hbox
+ 1
+
+ true
+
+
+
+
+
+ true
+
+
+
+
+
+
+ table
+ 2
+
+
+ 150
+ 0
+ 0
+
+
+
+
+ right
+ 1
+ 0
+
+
+
+
+ left
+ %.5f
+ /controls/flight/aileron
+ 1
+ 2
+ 0
+
+
+
+
+ right
+ 1
+ 1
+
+
+
+
+ left
+ %.5f
+ /controls/flight/elevator
+ 1
+ 2
+ 1
+
+
+
+
+ right
+ 3
+ 0
+
+
+
+
+ left
+ %.5f
+ /controls/flight/rudder
+ 1
+ 4
+ 0
+
+
+
+
+ right
+ 3
+ 1
+
+
+
+
+ left
+ %.5f
+ /controls/engines/engine/throttle
+ 1
+ 4
+ 1
+
+
+
+ 150
+ 5
+ 0
+
+
+
+
+
+ table
+ left
+
+
+ 0
+ 0
+ right
+
+
+
+
+ jsselect
+ 0
+ 1
+ left
+ /sim/gui/dialogs/joystick-config/selected-joystick
+ 350
+
+ dialog-apply
+ jsselect
+
+
+ nasal
+
+
+
+
+
+ 1
+ 0
+ right
+
+
+
+
+ 1
+ 1
+ left
+
+ /sim/gui/dialogs/joystick-config/selected-joystick-config
+
+
+
+
+
+
+
+
+ hbox
+ top
+ fill
+
+
+
+ table
+ top
+ axistable
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 1
+
+
+
+
+ 0
+ 2
+
+
+
+
+ 0
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+ table
+ top
+ buttontable
+
+
+
+ 0
0
+
+
+
+
+ 0
1
+
+
+
+
+ 0
2
+
+
+
+
+ 0
3
+
+
+
+
+ 0
4
+
+
+
+
+ 0
5
+
+
+
+
+ buttontemplate
+
+
+ 1
0
+ right
+ 1
+ 1
+ /devices/status/joysticks/joystick[0]/button[0]
+
+
+
+ 1
1
+ /sim/gui/dialogs/joystick-config/button[0]
+
+ left
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hbox
+
+
+ true
+
+
+
+
+
+ true
+
+
+
+