Using listeners and signals with Nasal: Difference between revisions

Jump to navigation Jump to search
→‎Listeners and Signals: due for an update... document args to callbacks
m (Use Nasal highlighter)
(→‎Listeners and Signals: due for an update... document args to callbacks)
Line 39: Line 39:


===setlistener() vs. _setlistener() ===
===setlistener() vs. _setlistener() ===
You are requested *not* to use the raw _setlistener() function, except in files in [[$FG_ROOT]]/Nasal/ when they are
You are requested '''not''' to use the raw _setlistener() function, except in files in [[$FG_ROOT]]/Nasal/ when they are
needed immediately. Only then the raw function is required, as it doesn't rely on props.nas.
needed immediately. Only then the raw function is required, as it doesn't rely on props.nas. Using setlistener() once props.nas is loaded allows using high-level objects to reference properties, instead of raw C-objects (called "ghosts").
 
'''Note:''' Once [[Nasal/CppBind|cppbind]] is used to replace props.nas, _setlistener() will be deprecated because the builtin function will be effectively using the same mechanism as what the wrapper function (the current setlistener()) does right now.


===<tt>When listeners don't work</tt>===
===<tt>When listeners don't work</tt>===
Unfortunately, '''listeners don't work on so-called "tied" properties''' when the node value isn't set via property methods. (You can spot such tied properties by Ctrl-clicking the "." entry in the property browser: they are marked with a "T".) Most of the FDM properties are "tied".
Unfortunately, '''listeners don't work on so-called "tied" properties''' when the node value isn't set via property methods. Tied properties are a semi-deprecated API to allow C++ code to handle the value directly and control getting/setting directly, usually avoiding the property tree altogether. (You can spot such tied properties by Ctrl-clicking the "." entry in the property browser: they are marked with a "T".) The problem comes when the C++ value is written to outside of the property tree, which means that the property tree doesn't receive a notification, even though normal sets via the property tree would still fire the listeners. Most of the FDM properties are "tied", and a few in other subsystems.


Examples of properties where setlistener ''won't'' work:  
Examples of properties where setlistener ''won't'' work:  
Line 62: Line 64:
Syntax:
Syntax:


var listener_id = setlistener(<property>, <function> [, <startup=0> [, <runtime=1>]]);
<syntaxhighlight lang="nasal">
var listener_id = setlistener(<property>, <function> [, <startup=0> [, <runtime=1>]]);
</syntaxhighlight>


The first argument is a property node object (<tt>props.Node()</tt> hash) or a property path. Because the node hash depends on the props.nas module being loaded, <tt>setlistener()</tt> calls need to be deferred when used in an [[$FG_ROOT]]/Nasal/*.nas file, usually by calling them in a <tt>settimer(func {}, 0)</tt> construction. To avoid that, one can use the raw <tt>_setlistener()</tt> function directly, for which <tt>setlistener()</tt> is a wrapper. The raw function does only accept node paths (e.g. "/sim/menubar/visibility"), but not props.Node() objects.
The first argument is a property node object (<tt>props.Node()</tt> hash) or a property path. Because the node hash depends on the props.nas module being loaded, <tt>setlistener()</tt> calls need to be deferred when used in an [[$FG_ROOT]]/Nasal/*.nas file, usually by calling them in a <tt>settimer(func {}, 0)</tt> construction. To avoid that, one can use the raw <tt>_setlistener()</tt> function directly, for which <tt>setlistener()</tt> is a wrapper. The raw function does only accept node paths (e.g. "/sim/menubar/visibility"), but not props.Node() objects.
Line 79: Line 83:
Here's a real-life example:
Here's a real-life example:


  setlistener("/gear/launchbar/state", func {
<syntaxhighlight lang="nasal">
      if (cmdarg().getValue() == "Engaged")
setlistener("/gear/launchbar/state", func {
          setprop("/sim/messages/copilot", "Engaged!");
    if (cmdarg().getValue() == "Engaged")
  }, 1, 0);
        setprop("/sim/messages/copilot", "Engaged!");
}, 1, 0);
</syntaxhighlight>


YASim writes once per frame the string "Disengaged" to property /gear/launchbar/state. When an aircraft on deck of the aircraft carrier locks into the catapult, this changes to "Engaged", which is then written again in every frame, until the aircraft leaves the catapult. Because the locking in is a bit difficult -- one has to target the sensitive area quite exactly --, it was desirable to get some quick feedback: a screen message that's also spoken by the Festival speech synthesis. With the args 1 and 0, this is done initially (for the unlikely case that we are locked in from the beginning), and then only when the node changes from an arbitrary value to "Engaged".
YASim writes once per frame the string "Disengaged" to property /gear/launchbar/state. When an aircraft on deck of the aircraft carrier locks into the catapult, this changes to "Engaged", which is then written again in every frame, until the aircraft leaves the catapult. Because the locking in is a bit difficult -- one has to target the sensitive area quite exactly --, it was desirable to get some quick feedback: a screen message that's also spoken by the Festival speech synthesis. With the args 1 and 0, this is done initially (for the unlikely case that we are locked in from the beginning), and then only when the node changes from an arbitrary value to "Engaged".
Line 88: Line 94:
<tt>setlistener()</tt> returns a unique listener id on success, and <tt>nil</tt> on error. The id is nothing else than a counter that is 0 for the first Nasal listener, 1 for the second etc. You need this id number to remove the listener. Most listeners are never removed, so that one doesn't assign the return value, but simply drop it.
<tt>setlistener()</tt> returns a unique listener id on success, and <tt>nil</tt> on error. The id is nothing else than a counter that is 0 for the first Nasal listener, 1 for the second etc. You need this id number to remove the listener. Most listeners are never removed, so that one doesn't assign the return value, but simply drop it.


Listener callback functions can access up to four values via regular function arguments, the first two of which are property nodes in the form of a <tt>props.Node()</tt> object hash.
Listener callback functions can access up to four values via regular function arguments, the first two of which are property nodes in the form of a <tt>props.Node()</tt> object hash. The third is a indication of the operation: 0 for changing the value, -1 for removing a child node, and +1 for adding a child. The fourth indicates whether the event occurred on the node that was listened to and is always 0 if the previous argument is not 0.


If you have set a callback function named ''myCallbackFunc'' via <tt>setlistener</tt> (''setlistener(myNode, myCallbackFunc)''), you can use this syntax in the callback function:
Here is the syntax supposing you have set a callback function named ''myCallbackFunc'' via <tt>setlistener</tt> (''setlistener(myNode, myCallbackFunc)''):


myCallbackFunc ([<changed_node> [, <listened_to_node> [, <operation> [, <is_child_event>]]]])
<syntaxhighlight lang="nasal">
myCallbackFunc([<changed_node> [, <listened_to_node> [, <operation> [, <is_child_event>]]]])
</syntaxhighlight>


=== <tt>removelistener()</tt> ===
=== <tt>removelistener()</tt> ===
Line 98: Line 106:
Syntax:
Syntax:


var num_listeners = removelistener(<listener id>);
<syntaxhighlight lang="nasal">
var num_listeners = removelistener(<listener id>);
</syntaxhighlight>


<tt>removelistener()</tt> takes one argument: the unique listener id that a <tt>setlistener()</tt> call returned. It returns the number of remaining active Nasal listeners on success, <tt>nil</tt> on error, or -1 if a listener function applies <tt>removelistener()</tt> to itself. The fact that a listener can remove itself, can be used to implement a one-shot listener function:
<tt>removelistener()</tt> takes one argument: the unique listener id that a <tt>setlistener()</tt> call returned. It returns the number of remaining active Nasal listeners on success, <tt>nil</tt> on error, or -1 if a listener function applies <tt>removelistener()</tt> to itself. The fact that a listener can remove itself, can be used to implement a one-shot listener function:


var L = setlistener("/some/property", func {
<syntaxhighlight lang="nasal">
    print("I can only be triggered once.");
var L = setlistener("/some/property", func {
    removelistener(L);
    print("I can only be triggered once.");
});
    removelistener(L);
 
});
</syntaxhighlight>




Line 113: Line 124:
The following example attaches an anonymous callback function to a "signal". The function will be executed when FlightGear is closed.
The following example attaches an anonymous callback function to a "signal". The function will be executed when FlightGear is closed.


setlistener("/sim/signals/exit", func { print("bye!") });
<syntaxhighlight lang="nasal">
 
setlistener("/sim/signals/exit", func { print("bye!") });
</syntaxhighlight>


Instead of an anonymous function, a named function can be used as well:
Instead of an anonymous function, a named function can be used as well:


var say_bye = func { print("bye") }
<syntaxhighlight lang="nasal">
setlistener("/sim/signals/exit", say_bye);
var say_bye = func { print("bye") }
setlistener("/sim/signals/exit", say_bye);
</syntaxhighlight>


Callback functions can, optionally, access up to four parameters which are handed over via regular function arguments. Many times none of these parameters is used at all, as in the above example.
Callback functions can, optionally, access up to four parameters which are handed over via regular function arguments. Many times none of these parameters is used at all, as in the above example.
Line 127: Line 141:
The following code attaches the monitor_course() function to a gps property, using the argument ''course'' to get the node with the changed value.
The following code attaches the monitor_course() function to a gps property, using the argument ''course'' to get the node with the changed value.


var monitor_course = func(course) {
<syntaxhighlight lang="nasal">
    print("Monitored course set to ", course.getValue());
var monitor_course = func(course) {
}
    print("Monitored course set to ", course.getValue());
var i = setlistener("instrumentation/gps/wp/leg-course-deviation-deg", monitor_course);
}
var i = setlistener("instrumentation/gps/wp/leg-course-deviation-deg", monitor_course);
# here the listener is active
 
# here the listener is active
removelistener(i);                    # remove that listener again
 
removelistener(i);                    # remove that listener again
</syntaxhighlight>


Here is code that accesses two arguments--the changed node and the listened-to node (these may be different when monitoring all children of a certain node)--and also shows how to monitor changes to a node including changes to children:
Here is code that accesses two arguments--the changed node and the listened-to node (these may be different when monitoring all children of a certain node)--and also shows how to monitor changes to a node including changes to children:


var monitor_course = func(course, flightinfo) {
<syntaxhighlight lang="nasal">
    print("One way to get the course setting: ", flightinfo.leg-course-deviation-deg.getValue());
var monitor_course = func(course, flightinfo) {
    print("Another way to get the same setting ", course.getValue());
    print("One way to get the course setting: ", flightinfo.leg-course-deviation-deg.getValue());
}
    print("Another way to get the same setting ", course.getValue());
var i = setlistener("instrumentation/gps/wp", monitor_course, 0, 2);
}
 
var i = setlistener("instrumentation/gps/wp", monitor_course, 0, 2);
</syntaxhighlight>


The function object doesn't need to be a separate, external function -- it can also be an anonymous function made directly in the <tt>setlistener()</tt> call:
The function object doesn't need to be a separate, external function -- it can also be an anonymous function made directly in the <tt>setlistener()</tt> call:


setlistener("/sim/signals/exit", func { print("bye") });    # say "bye" on exit
<syntaxhighlight lang="nasal">
setlistener("/sim/signals/exit", func { print("bye") });    # say "bye" on exit
</syntaxhighlight>


Beware, however, that the contents of a function defined within the <tt>setlistener</tt> call are not evaluated until the call is actually made. If, for instance, local variables change before the setlistener call happens, the call will reflect the current value of those variables ''at the time the callback function is called'', not the value ''at the time the listener was set''.  
Beware, however, that the contents of a function defined within the <tt>setlistener</tt> call are not evaluated until the call is actually made. If, for instance, local variables change before the setlistener call happens, the call will reflect the current value of those variables ''at the time the callback function is called'', not the value ''at the time the listener was set''.  
Line 153: Line 172:
For example, with this loop, the function will always return the value 10--even if mynode[1], mynode[2], mynode[3] or any of the others is the one that changed. It is because the contents of the setlistener are evaluated after the loop has completed running and at that point, i=10:
For example, with this loop, the function will always return the value 10--even if mynode[1], mynode[2], mynode[3] or any of the others is the one that changed. It is because the contents of the setlistener are evaluated after the loop has completed running and at that point, i=10:


var output = func(number) {
<syntaxhighlight lang="nasal">
    print("mynode", number, " has changed!"); #This won't work!
var output = func(number) {
}
    print("mynode", number, " has changed!"); #This won't work!
for(i=1; i <= 10; i = i+1) {
}
    var i = setlistener("mynode["~i~"]", func{ output (i); });
for(i=1; i <= 10; i = i+1) {
}
  var i = setlistener("mynode["~i~"]", func{ output (i); });
}
</syntaxhighlight>


You can also access the four available function properties (or just one, two, or three of them as you need) in your anonymous function. Here is an example that accesses the first value:
You can also access the four available function properties (or just one, two, or three of them as you need) in your anonymous function. Here is an example that accesses the first value:
 
for(i=1; i <= 10; i = i+1) {
<syntaxhighlight lang="nasal">
for(i=1; i <= 10; i = i+1) {
     var i = setlistener("mynode["~i~"]", func (changedNode) { print (changedNode.getPath() ~ " : " ~ changedNode.getValue()); });
     var i = setlistener("mynode["~i~"]", func (changedNode) { print (changedNode.getPath() ~ " : " ~ changedNode.getValue()); });
}
}
</syntaxhighlight>


Attaching a function to a node that is specified as <tt>props.Node()</tt> hash:
Attaching a function to a node that is specified as <tt>props.Node()</tt> hash:


var node = props.globals.getNode("/sim/signals/click", 1);
<syntaxhighlight lang="nasal">
setlistener(node, func { gui.popupTip("don't click here!") });
var node = props.globals.getNode("/sim/signals/click", 1);
setlistener(node, func { gui.popupTip("don't click here!") });
</syntaxhighlight>


Sometimes it is desirable to call the listener function initially, so that it can pick up the node value. In the following example a listener watches the view number, and turns the HUD on in cockpit view, and off in all other views. It doesn't only do that on writing to "view-number", but also once when the listener gets attached, thanks to the third argument "1":
Sometimes it is desirable to call the listener function initially, so that it can pick up the node value. In the following example a listener watches the view number, and turns the HUD on in cockpit view, and off in all other views. It doesn't only do that on writing to "view-number", but also once when the listener gets attached, thanks to the third argument "1":


setlistener("/sim/current-view/view-number", func(n) {
<syntaxhighlight lang="nasal">
    setprop("/sim/hud/visibility[0]", n.getValue() == 0);
setlistener("/sim/current-view/view-number", func(n) {
}, 1);
    setprop("/sim/hud/visibility[0]", n.getValue() == 0);
}, 1);
</syntaxhighlight>


There's no limit for listeners on a node. Several functions can get attached to one node, just as one function can get attached to several nodes. Listeners may write to the node they are listening to. This will not make the listener call itself causing an endless recursion.
There's no limit for listeners on a node. Several functions can get attached to one node, just as one function can get attached to several nodes. Listeners may write to the node they are listening to. This will not make the listener call itself causing an endless recursion.
395

edits

Navigation menu