@qooxdoo/framework
Version:
The JS Framework for Coders
927 lines (829 loc) • 28.9 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2006, 2007, 2011 Derrell Lipman
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Derrell Lipman (derrell)
************************************************************************ */
/**
* Create a new state which may be added to a finite state machine.
*/
qx.Class.define("qx.util.fsm.State",
{
extend : qx.core.Object,
/**
* @param stateName {String}
* The name of this state. This is the name which may be referenced in
* objects of class qx.util.fsm.Transition, when passing of
* the transition's predicate means transition to this state.
*
* @param stateInfo {Map}
* <pre>
* An object containing any of the following properties:
*
* context -
* A context in which all of the following functions should be run.
*
* onentry -
* A function which is called upon entry to the state. Its signature
* is function(fsm, event) and it is saved in the onentry property of
* the state object. (This function is called after the Transition's
* action function and after the previous state's onexit function.)
*
* In the onentry function:
*
* fsm -
* The finite state machine object to which this state is attached.
*
* event -
* The event that caused the finite state machine to run
*
* onexit -
* A function which is called upon exit from the state. Its signature
* is function(fsm, event) and it is saved in the onexit property of
* the state object. (This function is called after the Transition's
* action function and before the next state's onentry function.)
*
* In the onexit function:
*
* fsm -
* The finite state machine object to which this state is attached.
*
* event -
* The event that caused the finite state machine to run
*
* autoActionsBeforeOnentry -
* autoActionsAfterOnentry -
* autoActionsBeforeOnexit -
* autoActionsAfterOnexit -
* Automatic actions which take place at the time specified by the
* property name. In all cases, the action takes place immediately
* before or after the specified function.
*
* The property value for each of these properties is an object which
* describes some number of functions to invoke on a set of specified
* objects (typically widgets).
*
* An example, using autoActionsBeforeOnentry, might look like this:
*
* "autoActionsBeforeOnentry" :
* {
* // The name of a function.
* "setEnabled" :
* [
* {
* // The parameter value, thus "setEnabled(true);"
* "parameters" : [ true ],
*
* // The function would be called on each object:
* // this.getObject("obj1").setEnabled(true);
* // this.getObject("obj2").setEnabled(true);
* "objects" : [ "obj1", "obj2" ],
*
* // And similarly for each object in each specified group.
* "groups" : [ "group1", "group2" ]
* }
* ],
*
* // The name of another function.
* "setVisible" :
* [
* {
* // The parameter value, thus "setVisible(false);"
* "parameters" : [ false ],
*
* // The function would be called on each object and group, as
* // described above.
* "objects" : [ "obj3", "obj4" ],
* "groups" : [ "group3", "group4" ]
* }
* ]
* };
*
* events (required) -
* A description to the finite state machine of how to handle a
* particular event, optionally associated with a specific target
* object on which the event was dispatched. This should be an object
* containing one property for each event which is either handled or
* blocked. The property name should be the event name. The property
* value should be one of:
*
* (a) qx.util.fsm.FiniteStateMachine.EventHandling.PREDICATE
*
* (b) qx.util.fsm.FiniteStateMachine.EventHandling.BLOCKED
*
* (c) a string containing the name of an explicit Transition to use
*
* (d) an object where each property name is the Friendly Name of an
* object (meaning that this rule applies if both the event and
* the event's target object's Friendly Name match), and its
* property value is one of (a), (b) or (c), above.
*
* This object is saved in the events property of the state object.
*
* Additional properties may be provided in stateInfo. They will not be
* used by the finite state machine, but will be available via
* this.getUserData("<propertyName>") during the state's onentry and
* onexit functions.
* </pre>
*
* @throws {Error} If the state info is not a valid object.
* @throws {Error} If the events object is not provided in new state info.
*
*/
construct : function(stateName, stateInfo)
{
var context;
// Call our superclass' constructor
this.base(arguments);
// Save the state name
this.setName(stateName);
// Ensure they passed in an object
if (typeof (stateInfo) != "object") {
throw new Error("State info must be an object");
}
// If a context was specified, retrieve it.
context = stateInfo.context || window;
// Save it for future use
this.setUserData("context", context);
// Save data from the stateInfo object
for (var field in stateInfo)
{
// If we find one of our properties, call its setter.
switch(field)
{
case "onentry":
this.setOnentry(
this.__bindIfFunction(stateInfo[field], context));
break;
case "onexit":
this.setOnexit(
this.__bindIfFunction(stateInfo[field], context));
break;
case "autoActionsBeforeOnentry":
this.setAutoActionsBeforeOnentry(stateInfo[field]);
break;
case "autoActionsAfterOnentry":
this.setAutoActionsAfterOnentry(stateInfo[field]);
break;
case "autoActionsBeforeOnexit":
this.setAutoActionsBeforeOnexit(stateInfo[field]);
break;
case "autoActionsAfterOnexit":
this.setAutoActionsAfterOnexit(stateInfo[field]);
break;
case "events":
this.setEvents(stateInfo[field]);
break;
case "context":
// already handled
break;
default:
// Anything else is user-provided data for their own use. Save it.
this.setUserData(field, stateInfo[field]);
// Log it in case it was a typo and they intended a built-in field
this.debug("State " + stateName + ": " +
"Adding user-provided field to state: " + field);
break;
}
}
// Check for required but missing properties
if (!this.getEvents()) {
throw new Error("The events object must be provided in new state info");
}
// Initialize the transition list
this.transitions = {};
},
statics :
{
/**
* Common function for checking the value provided for
* auto actions.
*
* Auto-action property values passed to us look akin to:
*
* <pre class='javascript'>
* {
* // The name of a function.
* "setEnabled" :
* [
* {
* // The parameter value(s), thus "setEnabled(true);"
* "parameters" : [ true ],
*
* // The function would be called on each object:
* // this.getObject("obj1").setEnabled(true);
* // this.getObject("obj2").setEnabled(true);
* "objects" : [ "obj1", "obj2" ]
*
* // And similarly for each object in each specified group.
* "groups" : [ "group1", "group2" ],
* }
* ];
*
* "setTextColor" :
* [
* {
* "parameters" : [ "blue" ]
* "groups" : [ "group3", "group4" ],
* "objects" : [ "obj3", "obj4" ]
* }
* ];
* };
* </pre>
*
*
* @param actionType {String}
* The name of the action being validated (for debug messages)
*
* @param value {Object}
* The property value which is being validated
*
* @param context {Object}
* The object to which the created function should be bound.
*
* @return {Function}
* Function that implements calls to each of the requested automatic
* actions
*
* @throws {Error} If the value has an invalid type.
* @throws {Error} If the function type is not an array.
* @throws {Error} If the function request parameter type is not valid.
* @throws {Error} If the function parameters are not valid.
* @throws {Error} If 'objects' list is invalid.
* @throws {Error} If a name in the 'objects' list is not valid.
* @throws {Error} If the 'groups' list is not valid.
*/
_commonTransformAutoActions : function(actionType, value, context)
{
// Validate that we received an object property value
if (typeof (value) != "object") {
throw new Error("Invalid " + actionType + " value: " +
typeof (value));
}
// We'll create a function to do the requested actions. Initialize the
// string into which we'll generate the common fragment added to the
// function for each object.
var funcFragment;
// Here, we'll keep the function body. Initialize a try block.
var func = "try" + "{";
var param;
var objectAndGroupList;
// Retrieve the function request, e.g.
// "enabled" :
for (var f in value)
{
// Get the function request value object, e.g.
// "setEnabled" :
// [
// {
// "parameters" : [ true ],
// "objects" : [ "obj1", "obj2" ]
// "groups" : [ "group1", "group2" ],
// }
// ];
var functionRequest = value[f];
// The function request value should be an object
if (!functionRequest instanceof Array) {
throw new Error("Invalid function request type: " +
"expected array, found " +
typeof (functionRequest));
}
// For each function request...
for (var i=0; i<functionRequest.length; i++)
{
// Retrieve the object and group list object
objectAndGroupList = functionRequest[i];
// The object and group list should be an object, e.g.
// {
// "parameters" : [ true ],
// "objects" : [ "obj1", "obj2" ]
// "groups" : [ "group1", "group2" ],
// }
if (typeof (objectAndGroupList) != "object") {
throw new Error("Invalid function request parameter type: " +
"expected object, found " +
typeof (functionRequest[param]));
}
// Retrieve the parameter list
var params = objectAndGroupList["parameters"];
// If it didn't exist, ...
if (!params)
{
// ... use an empty array.
params = [];
}
else
{
// otherwise, ensure we got an array
if (!params instanceof Array) {
throw new Error("Invalid function parameters: " +
"expected array, found " +
typeof (params));
}
}
// Create the function to call on each object. The object on which
// the function is called will be prepended later.
funcFragment = f + "(";
// For each parameter...
for (var j=0; j<params.length; j++)
{
// If this isn't the first parameter, add a separator
if (j != 0) {
funcFragment += ",";
}
if (typeof (params[j]) == "function")
{
// If the parameter is a function, arrange for it to be called
// at run time.
funcFragment += "(" + params[j] + ")(fsm)";
}
else if (typeof (params[j]) == "string")
{
// If the parameter is a string, quote it.
funcFragment += '"' + params[j] + '"';
}
else
{
// Otherwise, just add the parameter's literal value
funcFragment += params[j];
}
}
// Complete the function call
funcFragment += ")";
// Get the "objects" list, e.g.
// "objects" : [ "obj1", "obj2" ]
var a = objectAndGroupList["objects"];
// Was there an "objects" list?
if (!a)
{
// Nope. Simplify code by creating an empty array.
a = [];
}
else if (!a instanceof Array)
{
throw new Error("Invalid 'objects' list: expected array, got " +
typeof (a));
}
for (var j=0; j<a.length; j++)
{
// Ensure we got a string
if (typeof (a[j]) != "string") {
throw new Error("Invalid friendly name in 'objects' list: " +
a[j]);
}
func += " fsm.getObject('" + a[j] + "')." + funcFragment + ";";
}
// Get the "groups" list, e.g.
// "groups" : [ "group1, "group2" ]
var g = objectAndGroupList["groups"];
// Was a "groups" list found?
if (g)
{
// Yup. Ensure it's an array.
if (!g instanceof Array)
{
throw new Error("Invalid 'groups' list: expected array, got " +
typeof (g));
}
for (j=0; j<g.length; j++)
{
// Arrange to call the function on each object in each group
func +=
" var groupObjects = " +
" fsm.getGroupObjects('" + g[j] + "');" +
" for (var i = 0; i < groupObjects.length; i++)" +
" {" +
" var objName = groupObjects[i];" +
" fsm.getObject(objName)." + funcFragment + ";" +
" }";
}
}
}
}
// Terminate the try block for function invocations
func += "}" + "catch(ex)" + "{" + " fsm.debug(ex);" + "}";
// We've now built the entire body of a function that implements calls
// to each of the requested automatic actions. Create and return the
// function, which will become the property value.
return qx.lang.Function.bind(new Function("fsm", func), context);
}
},
properties :
{
/**
* The name of this state. This name may be used as a Transition's
* nextState value, or an explicit next state in the 'events' handling
* list in a State.
*/
name :
{
transform : "__transformName",
nullable : true
},
/**
* The onentry function for this state. This is documented in the
* constructor, and is typically provided through the constructor's
* stateInfo object, but it is also possible (but highly NOT recommended)
* to change this dynamically.
*/
onentry :
{
transform : "__transformOnentry",
nullable : true,
init : function(fsm, event) {}
},
/**
* The onexit function for this state. This is documented in the
* constructor, and is typically provided through the constructor's
* stateInfo object, but it is also possible (but highly NOT recommended)
* to change this dynamically.
*/
onexit :
{
transform : "__transformOnexit",
nullable : true,
init : function(fsm, event) {}
},
/**
* Automatic actions to take prior to calling the state's onentry function.
*
* The value passed to setAutoActionsBeforeOnentry() should like something
* akin to:
*
* <pre class='javascript'>
* "autoActionsBeforeOnentry" :
* {
* // The name of a function. This would become "setEnabled("
* "enabled" :
* [
* {
* // The parameter value, thus "setEnabled(true);"
* "parameters" : [ true ],
*
* // The function would be called on each object:
* // this.getObject("obj1").setEnabled(true);
* // this.getObject("obj2").setEnabled(true);
* "objects" : [ "obj1", "obj2" ]
*
* // And similarly for each object in each specified group.
* "groups" : [ "group1", "group2" ],
* }
* ];
* };
* </pre>
*/
autoActionsBeforeOnentry :
{
transform : "__transformAutoActionsBeforeOnentry",
nullable : true,
init : function(fsm, event) {}
},
/**
* Automatic actions to take after return from the state's onentry
* function.
*
* The value passed to setAutoActionsAfterOnentry() should like something
* akin to:
*
* <pre class='javascript'>
* "autoActionsAfterOnentry" :
* {
* // The name of a function. This would become "setEnabled("
* "enabled" :
* [
* {
* // The parameter value, thus "setEnabled(true);"
* "parameters" : [ true ],
*
* // The function would be called on each object:
* // this.getObject("obj1").setEnabled(true);
* // this.getObject("obj2").setEnabled(true);
* "objects" : [ "obj1", "obj2" ]
*
* // And similarly for each object in each specified group.
* "groups" : [ "group1", "group2" ],
* }
* ];
* };
* </pre>
*/
autoActionsAfterOnentry :
{
transform : "__transformAutoActionsAfterOnentry",
nullable : true,
init : function(fsm, event) {}
},
/**
* Automatic actions to take prior to calling the state's onexit function.
*
* The value passed to setAutoActionsBeforeOnexit() should like something
* akin to:
*
* <pre class='javascript'>
* "autoActionsBeforeOnexit" :
* {
* // The name of a function. This would become "setEnabled("
* "enabled" :
* [
* {
* // The parameter value, thus "setEnabled(true);"
* "parameters" : [ true ],
*
* // The function would be called on each object:
* // this.getObject("obj1").setEnabled(true);
* // this.getObject("obj2").setEnabled(true);
* "objects" : [ "obj1", "obj2" ]
*
* // And similarly for each object in each specified group.
* "groups" : [ "group1", "group2" ],
* }
* ];
* };
* </pre>
*/
autoActionsBeforeOnexit :
{
transform : "__transformAutoActionsBeforeOnexit",
nullable : true,
init : function(fsm, event) {}
},
/**
* Automatic actions to take after returning from the state's onexit
* function.
*
* The value passed to setAutoActionsAfterOnexit() should like something
* akin to:
*
* <pre class='javascript'>
* "autoActionsBeforeOnexit" :
* {
* // The name of a function. This would become "setEnabled("
* "enabled" :
* [
* {
* // The parameter value, thus "setEnabled(true);"
* "parameters" : [ true ],
*
* // The function would be called on each object:
* // this.getObject("obj1").setEnabled(true);
* // this.getObject("obj2").setEnabled(true);
* "objects" : [ "obj1", "obj2" ]
*
* // And similarly for each object in each specified group.
* "groups" : [ "group1", "group2" ],
* }
* ];
* };
* </pre>
*/
autoActionsAfterOnexit :
{
transform : "__transformAutoActionsAfterOnexit",
nullable : true,
init : function(fsm, event) {}
},
/**
* The object representing handled and blocked events for this state.
* This is documented in the constructor, and is typically provided
* through the constructor's stateInfo object, but it is also possible
* (but highly NOT recommended) to change this dynamically.
*/
events :
{
transform : "__transformEvents",
nullable : true
}
},
members :
{
/**
* Internal transform method
*
* @param value {var} Value passed to setter
* @return {var} the final value
* @throws {Error} when an invalid value is detected
*/
__transformName : function(value)
{
// Ensure that we got a valid state name
if (typeof (value) != "string" || value.length < 1)
{
throw new Error("Invalid state name");
}
return value;
},
/**
* Internal transform method
*
* @param value {var} Current value
* @return {var} the final value
* @throws {Error} when an invalid value is detected
*/
__transformOnentry : function(value)
{
// Validate the onentry function
switch(typeof (value))
{
case "undefined":
// None provided. Convert it to a null function
return function(fsm, event) {};
case "function":
// We're cool. No changes required
return qx.lang.Function.bind(value, this.getUserData("context"));
default:
throw new Error("Invalid onentry type: " + typeof (value));
}
},
/**
* Internal transform method
*
* @param value {var} Current value
* @return {var} the final value
* @throws {Error} when an invalid value is detected
*/
__transformOnexit : function(value)
{
// Validate the onexit function
switch(typeof (value))
{
case "undefined":
// None provided. Convert it to a null function
return function(fsm, event) {};
case "function":
// We're cool. No changes required
return qx.lang.Function.bind(value, this.getUserData("context"));
default:
throw new Error("Invalid onexit type: " + typeof (value));
}
},
/**
* Internal transform method
*
* @param value {var} Current value
* @return {var} the final value
* @throws {Error} when an invalid value is detected
*/
__transformEvents : function(value)
{
// Validate that events is an object
if (typeof (value) != "object") {
throw new Error("events must be an object");
}
// Confirm that each property is a valid value
// The property value should be one of:
//
// (a) qx.util.fsm.FiniteStateMachine.EventHandling.PREDICATE
//
// (b) qx.util.fsm.FiniteStateMachine.EventHandling.BLOCKED
//
// (c) a string containing the name of an explicit Transition to use
//
// (d) an object where each property name is the Friendly Name of an
// object (meaning that this rule applies if both the event and
// the event's target object's Friendly Name match), and its
// property value is one of (a), (b) or (c), above.
for (var e in value)
{
var action = value[e];
if (typeof (action) == "number" &&
action != qx.util.fsm.FiniteStateMachine.EventHandling.PREDICATE &&
action != qx.util.fsm.FiniteStateMachine.EventHandling.BLOCKED)
{
throw new Error("Invalid numeric value in events object: " +
e + ": " + action);
}
else if (typeof (action) == "object")
{
for (var action_e in action)
{
if (typeof (action[action_e]) == "number" &&
action[action_e] !=
qx.util.fsm.FiniteStateMachine.EventHandling.PREDICATE &&
action[action_e] !=
qx.util.fsm.FiniteStateMachine.EventHandling.BLOCKED)
{
throw new Error("Invalid numeric value in events object " +
"(" + e + "): " +
action_e + ": " +
action[action_e]);
}
else if (typeof (action[action_e]) != "string" &&
typeof (action[action_e]) != "number")
{
throw new Error("Invalid value in events object " +
"(" + e + "): " +
action_e + ": " + action[action_e]);
}
}
}
else if (typeof (action) != "string" && typeof (action) != "number")
{
throw new Error("Invalid value in events object: " +
e + ": " + value[e]);
}
}
// We're cool. No changes required.
return value;
},
/**
* Internal transform method
*
* @param value {var} Current value
* @return {var} the final value
*/
__transformAutoActionsBeforeOnentry : function(value)
{
return qx.util.fsm.State._commonTransformAutoActions(
"autoActionsBeforeOnentry",
value,
this.getUserData("context"));
},
/**
* Internal transform method
*
* @param value {var} Current value
* @return {var} the final value
*/
__transformAutoActionsAfterOnentry : function(value)
{
return qx.util.fsm.State._commonTransformAutoActions(
"autoActionsAfterOnentry",
value,
this.getUserData("context"));
},
/**
* Internal transform method
*
* @param value {var} Current value
* @return {var} the final value
*/
__transformAutoActionsBeforeOnexit : function(value)
{
return qx.util.fsm.State._commonTransformAutoActions(
"autoActionsBeforeOnexit",
value,
this.getUserData("context"));
},
/**
* Internal transform method
*
* @param value {var} Current value
* @return {var} the final value
*/
__transformAutoActionsAfterOnexit : function(value)
{
return qx.util.fsm.State._commonTransformAutoActions(
"autoActionsAfterOnexit",
value,
this.getUserData("context"));
},
/**
* If given a function, bind it to a specified context.
*
* @param f {Function|var}
* The (possibly) function to be bound to the specified context.
*
* @param context {Object}
* The context to bind the function to.
*
* @return {Function}
* If f was a function, the return value is f wrapped such that it will
* be called in the specified context. Otherwise, f is returned
* unaltered.
*/
__bindIfFunction : function(f, context)
{
// Is the first parameter a function?
if (typeof(f) == "function")
{
// Yup. Bind it to the specified context.
f = qx.lang.Function.bind(f, context);
}
return f;
},
/**
* Add a transition to a state
*
*
* @param trans {qx.util.fsm.Transition}
* An object of class qx.util.fsm.Transition representing a transition
* which is to be a part of this state.
*
*/
addTransition : function(trans)
{
// Ensure that we got valid transition info
if (!trans instanceof qx.util.fsm.Transition) {
throw new Error("Invalid transition: not an instance of " +
"qx.util.fsm.Transition");
}
// Add the new transition object to the state
this.transitions[trans.getName()] = trans;
}
}
});